Search code examples
vue.jsasynchronousvuejs3composable

Wait on async function to complete in a Vue composable before reading it's contents


I've writen multiple composables that will fetch specific objects from an API which I use in a component. I need to associate objects from one composable with objects in another composable. However I want to keep the fetch in the composables async so that the component is not waiting on the first to complete before starting the second.

The problem occurs when the API queries have not yet completed and values are not yet set, but the component tries to iterate over their values.

How can I accomplish this task?

The code I have sofar is as follows:

Component code:

const { customers } = useCustomers()
const { datacenters } = useDatacenters()

for (let customer of customers.value) {
  customer.datacenters = datacenters.filter(datacenter => datacenter.customer === customer.id)
}

Composable code:

function useCustomers() {
  const customers = ref([])

  async function updateCustomers(){
    customers.value = (await api.get("customers")).json
  }

  onMounted(() => {
    void updateCustomers()
  })

  return {
    customers,
    updateCustomers,

  }
}

function useDatacenters() {
  const datacenters = ref([])

  async function updateDatacenters() {
    datacenters.value = (await api.get("datacenter")).json
  }

  onMounted(() => {
    void updateDatacenters()
  })

  return {
    datacenters,
    updateDatacenters
  }
}

Solution

    1. You should watch for loading your composables. Either checks lengths of the arrays, or for example init them with null (you can also provide loaded refs from the composables like customersLoaded).
    const customers = ref(null)
    
    1. Then just watch for the both loaded:
    const off = watch([customers, datacenters], () => {
      if(!customers.value || !datacenters.value) return; // not loaded yet
      off();
      for (let customer of customers.value) {
        customer.datacenters = datacenters.value.filter(datacenter => datacenter.customer === customer.id)
      }
    });
    
    1. You can create a utility function that waits for any number of composables like watchLoaded(). A bonus would be to make it async:
    export async function watchLoaded(refs, cb) {
      return new Promise(resolve => {
        const off = watch(refs, async values => refs.every(ref => ref.value) && (off(), resolve(await cb(values))));
      });
    }
    
    1. But you should better create a utility to create your composables with full feature support like loading status and error handling:

    VUE SFC PLAYGROUND

    function createLoader(loader){
     const error = ref(null);
     const loaded = ref(false);
    
     const fn = Object.values(loader).find(item => typeof item === 'function');
    
     onMounted(async () => {
       try{
         await fn();
         loaded.value = true;
       }catch(e){
         error.value = e.message;
       }
     });
    
     return {...loader, loaded, error, loading: computed(() => !loaded.value && !error.value)};
     
    }
    
    function useCustomers() {
     const customers = ref([]);
     const updateCustomers = async () => customers.value = await fakeapi([{name: 'Alexander', id: 1}]);
     return createLoader({customers, updateCustomers});
    }
    
    function useDatacenters() {
     const datacenters = ref([]);
     const updateDatacenters = async () => datacenters.value = await fakeapi([{location: 'North', customer: 1}]);
     return createLoader({datacenters, updateDatacenters});
    }
    
    const { customers, ...customersLoader } = useCustomers();
    const { datacenters, ...datacentersLoader} = useDatacenters();
    
    function watchLoaded(loaders, cb) {
     const loaded = ref(false);
     const errors = ref([]);
     const off = watch(reactive(loaders), async values => {
       if(!loaders.every(l => l.loaded.value)) return;
       off();
       await cb(values);
       errors.value = loaders.map(l => l.error.value).filter(Boolean);
       loaded.value = !errors.value.length;
     });
    
     return {loaded, errors};
    }
    
    const {loaded, errors} = watchLoaded([customersLoader, datacentersLoader], () => {
     for (let customer of customers.value) {
       customer.datacenters = datacenters.value.filter(datacenter => datacenter.customer === customer.id)
     }
    });