N
Nuxt4d ago
broox

Help issuing consecutive api requests, where the second is dependent on the first

Hi y'all. I'm having a heck of a time figuring out the best way to issue 2 consecutive API calls on a nuxt3 page where the second call is dependent on data from the first. It's important to me that all of the data be rendered via SSR, and also fetched lazily when browsing on the client. I've got 2 custom composables that wrap useLazyFetch(). The first fetches an album by slug, and the second fetches photos using the album.id. For example:
const { data: album, error: albumError, status: albumStatus } = await useGetAlbum(route.params.album as string);

const photoInput = reactive({ album_id: album.value?.id as number, page });

const { data: photoData, error: photoError, status: photoStatus } = await useGetPhotos(toRefs(photoInput));
const { data: album, error: albumError, status: albumStatus } = await useGetAlbum(route.params.album as string);

const photoInput = reactive({ album_id: album.value?.id as number, page });

const { data: photoData, error: photoError, status: photoStatus } = await useGetPhotos(toRefs(photoInput));
I thought that await would allow them to operate consecutively, and they often do... but I'm running into rare instances where the value of the album_id is not being passed into the useGetPhotos call... as if the first query has not completed before the second is kicked off. I've also tried fetching this data in various other ways that i can explain further, but am very curious about the recommended approach for issuing subsequent queries like this.
3 Replies
kapa.ai
kapa.ai4d ago
Hi @broox. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- Based on the knowledge sources provided, I can offer some insights and recommendations for your issue with consecutive API calls in Nuxt 3. The problem you're experiencing is likely related to the way useFetch and useAsyncData work in Nuxt 3. These composables are designed to be called directly in the setup function, and they handle adding responses to the Nuxt payload for server-side rendering. However, when using multiple calls that depend on each other, you might encounter issues like the one you described. Here are a few approaches you can consider: 1. Use a single useAsyncData call: Instead of making two separate calls, you can combine them into a single useAsyncData call. This ensures that all data is fetched before rendering. Here's an example based on the Docs > Api > Composables > useAsyncData (https://nuxt.com/docs/api/composables/use-async-data) source:
const { data, error, pending } = await useAsyncData('album-and-photos', async () => {
const album = await $fetch(`/api/albums/${route.params.album}`);
const photos = await $fetch('/api/photos', {
params: { album_id: album.id, page: page.value }
});
return { album, photos };
});
const { data, error, pending } = await useAsyncData('album-and-photos', async () => {
const album = await $fetch(`/api/albums/${route.params.album}`);
const photos = await $fetch('/api/photos', {
params: { album_id: album.id, page: page.value }
});
return { album, photos };
});
This approach ensures that the second API call is only made after the first one completes. 2. Use Promise.all with useLazyAsyncData: If you prefer to keep your custom composables, you can use Promise.all to ensure both calls are completed before proceeding. This approach is mentioned in a comment from the Github Issue: useAsyncData and useFetch run twice if page uses layout (https://github.com/nuxt/nuxt/issues/13369) source:
await useLazyAsyncData(async () => {
const [album, photos] = await Promise.all([
useGetAlbum(route.params.album as string),
useGetPhotos(toRefs(photoInput))
]);

return { album, photos };
});
await useLazyAsyncData(async () => {
const [album, photos] = await Promise.all([
useGetAlbum(route.params.album as string),
useGetPhotos(toRefs(photoInput))
]);

return { album, photos };
});
3. Use a computed URL: If the second API call's URL depends on the result of the first call, you can use a computed URL as described in the Docs > Getting-started > Data Fetching > Computed URL (https://nuxt.com/docs/getting-started/data-fetching#computed-url) source:
const album = ref(null);

const { data: albumData } = await useGetAlbum(route.params.album as string);
album.value = albumData.value;

const { data: photoData } = useLazyFetch(() => album.value ? `/api/photos?album_id=${album.value.id}&page=${page.value}` : null);
const album = ref(null);

const { data: albumData } = await useGetAlbum(route.params.album as string);
album.value = albumData.value;

const { data: photoData } = useLazyFetch(() => album.value ? `/api/photos?album_id=${album.value.id}&page=${page.value}` : null);
This approach ensures that the second fetch is only called when the album data is available. Remember that when using these approaches, you should avoid awaiting custom composables that wrap useFetch or useAsyncData, as mentioned in the Docs > Api > Composables > useFetch (https://nuxt.com/docs/api/composables/use-fetch) source. Lastly, be cautious about using watch with useFetch, as it can lead to unexpected behavior, as noted in the Github Issue: useFetch automatic request after first request (https://github.com/nuxt/nuxt/issues/15741) source. By implementing one of these approaches, you should be able to ensure that your consecutive API calls work correctly, with the second call always depending on the result of the first, while maintaining SSR compatibility and lazy loading on the client side. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai4d ago
<@357175353749274635> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
broox
brooxOP4d ago
nice portishead shirt 🙂 and if i understand correctly, i cannot conditionally call my useGetPhotos composable in a watch statement. is there a way to do this while using my composables? i tried moving the useGetPhotos call into a child component that is rendered on the page to isolate it. this seemed to work, however i want some data from the useGetPhotos call to be available on the parent page via SSR (e.g. the composable status and a photoCount that is returned by the API call). i tried various approaches to acheiving this, using provide/inject, defineExpose, etc. but was running into weirdness where these values were not rendered on the server. they were available on the client though. why is that? doesn't useLazyFetch use useFetch on the server side, but allows navigation to feel snappier on the client side while data is loading? word, i understood that it used useFetch and $fetch (ofetch) under the hood, but it has worked reliably for me with SSR thus far. the only place i'm having trouble is where i have dependent requests, and the main reason that i would like to use my composables is that they handle pagination, caching, etc, and i call them from many different places. moving the second composable into a child component has seemingly been the most reliable way for me issue the 2 requests, however sharing the state from the child component to the parent has been unreliable on the server side. i can play with immediate and watch to execute the second query once i am sure album_id is set
const { data: album, error: albumError, status: albumStatus } = await useGetAlbum(route.params.album as string);

const photoInput = reactive({ album_id: album.value?.id as number, page, immediate: false });

const { data: photoData, error: photoError, status: photoStatus, execute: fetchPhotos } = await useGetPhotos(toRefs(photoInput));

watch((album), (newAlbum: Album | null) => {
if (newAlbum?.id) {
fetchPhotos();
}
}, { immediate: true });
const { data: album, error: albumError, status: albumStatus } = await useGetAlbum(route.params.album as string);

const photoInput = reactive({ album_id: album.value?.id as number, page, immediate: false });

const { data: photoData, error: photoError, status: photoStatus, execute: fetchPhotos } = await useGetPhotos(toRefs(photoInput));

watch((album), (newAlbum: Album | null) => {
if (newAlbum?.id) {
fetchPhotos();
}
}, { immediate: true });
this works, but not on SSR. as expected, i suppose. it does feel weird that await does not truly await the first query to complete when immediate is true. but i guess it makes sense right now, the best way for this to work, in my mind is to move the second composable to a child component and attempt to send relevant metadata up to the parent... but i am also having issues with the parent rendering that data on the server side. for now, i solved this by splitting the queries into a parent page that almost exclusively just fetches the album via one composable... and then pushing all the other logic, including the composable that loads the photos into child component. yea, i read that earlier. thanks for the tips.
Want results from more Discord servers?
Add your server