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
}
}
null
(you can also provide loaded
refs from the composables like customersLoaded
).const customers = ref(null)
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)
}
});
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))));
});
}
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)
}
});