N
Nuxt5d ago
hackal

useFetch not updating data ref when new request is triggered.

This is my simplified version of composable
const projectCache = () => useState("projectCache", () => ref({}))

const useProject = async (projectId: Ref<string>) => {
const localId = ref(unref(projectId));

watch(projectId, (newId) => {
if (projectCache().value[newId]) {
// project already in cache, we can set localId immediately
localId.value = newId;
}
})

const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, { immediate: false })

watch(data, (updated) => {
// new data from api, update cache and set the ID
projectCache().value[data.value.id] = data.value;
localId.value = data.value.id;
})

// check if we have project in cache
if (!projectCache().value[unref(projectId)]) {
// cache miss, we make the request and save to cache
await execute();
if (data.value) {
// add/update data in cache
projectCache().value[data.value.id] = data.value
}
}

return {
// always return from cache
project: computed(() => projectCache().value[unref(localId)])
}
}
const projectCache = () => useState("projectCache", () => ref({}))

const useProject = async (projectId: Ref<string>) => {
const localId = ref(unref(projectId));

watch(projectId, (newId) => {
if (projectCache().value[newId]) {
// project already in cache, we can set localId immediately
localId.value = newId;
}
})

const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, { immediate: false })

watch(data, (updated) => {
// new data from api, update cache and set the ID
projectCache().value[data.value.id] = data.value;
localId.value = data.value.id;
})

// check if we have project in cache
if (!projectCache().value[unref(projectId)]) {
// cache miss, we make the request and save to cache
await execute();
if (data.value) {
// add/update data in cache
projectCache().value[data.value.id] = data.value
}
}

return {
// always return from cache
project: computed(() => projectCache().value[unref(localId)])
}
}
I have the following issue. I call the composable in two components and pass the same id (ref) as arg const { data } = await useProject(projectId) when the projectId changes I see that useFetch will fire two requests however watch(data, (updated) => { is called only in one instance. Why that could be?
10 Replies
kapa.ai
kapa.ai5d ago
Hi @hackal. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- The issue you're experiencing is related to how useFetch handles caching and key generation. When you use useFetch in multiple components with the same URL and options, Nuxt treats them as the same request and shares the data between them. The Problem The main issue in your code is that useFetch is being called in multiple components with the same parameters, causing them to share state. When the second component calls the same useFetch, it's using the same internal key, so only one of the watchers gets triggered when data changes. This is a common issue when wrapping useFetch in a custom composable. As mentioned in a Stack Overflow answer:
Generally as a rule guiding composables, they must be called only in; a setup function, Plugins, & Route Middleware. Trying to use this composable outside this scope will lead to bugs that affects performance of the app.
Stack Overflow Solutions Here are a few approaches to fix this issue: 1. Use a unique key for each useFetch call
const useProject = async (projectId: Ref<string>, uniqueKey?: string) => {
const localId = ref(unref(projectId));

const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, {
key: uniqueKey || `project-${unref(projectId)}-${Date.now()}`
})

// rest of your code
}
const useProject = async (projectId: Ref<string>, uniqueKey?: string) => {
const localId = ref(unref(projectId));

const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, {
key: uniqueKey || `project-${unref(projectId)}-${Date.now()}`
})

// rest of your code
}
Then when calling the composable, provide a unique key:
const { project } = await useProject(projectId, 'component-a-project')
const { project } = await useProject(projectId, 'component-a-project')
2. Use $fetch directly for mutations If you're using this composable for form submissions or other mutations, consider using $fetch directly instead of useFetch. This is recommended in the Nuxt documentation and community:
When sending data, using useFetch is often not the way to go, especially on interaction
GitHub Issue 3. Use immediate: false with manual execution You could also use the immediate: false option with useFetch to have more control over when the fetch happens:
const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, {
immediate: false,
key: `project-${unref(projectId)}`
})

// Then manually execute when needed
const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, {
immediate: false,
key: `project-${unref(projectId)}`
})

// Then manually execute when needed
This approach is documented in the Not immediate section of the Nuxt docs. Additional Considerations - The issue in GitHub #31406 mentions a similar problem where they needed to prevent fetch from being triggered in certain states, which was resolved by using lazy and immediate options together. - Remember that when using useFetch with a computed URL (as you're doing), changing the reactive value will automatically trigger a refetch, as mentioned in the Computed URL documentation. I hope this helps resolve your issue with the composable! ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai5d ago
<@77852149421191168> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
hackal
hackalOP5d ago
I'm sorry I forgot to add it in my example but it is there. I edited the original post. @kapa.ai @kapa.ai
kapa.ai
kapa.ai5d ago
Let me check the knowledge base for you again. One moment! :books: Sorry @hackal, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
hackal
hackalOP5d ago
@kapa.ai I have { immediate: false } there.
kapa.ai
kapa.ai5d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- I see that you have { immediate: false } in your code. This is an important detail I missed in my previous analysis. Let me address how this affects your situation. When using useFetch with immediate: false, the fetch won't automatically execute when the component mounts, but you'll need to manually call execute() to trigger the request. However, this doesn't solve the key issue with multiple components using the same composable. The main problem is still related to how useFetch handles caching and key generation. When you use the same URL pattern in multiple components, Nuxt treats them as the same request and shares the data between them, even with immediate: false. According to the documentation:
If you do not provide a key, then a key that is unique to the file name and line number of the instance of useAsyncData will be generated for you.
useAsyncData documentation Since useFetch is built on top of useAsyncData, the same principle applies. However, when wrapping useFetch in a custom composable, this automatic key generation may not work as expected across multiple component instances. To fix this issue, you should explicitly provide a unique key for each useFetch call:
const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, {
immediate: false,
key: `project-${unref(projectId)}-${Date.now()}` // or some other unique identifier
})
const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, {
immediate: false,
key: `project-${unref(projectId)}-${Date.now()}` // or some other unique identifier
})
Alternatively, you could pass a unique key as a parameter to your composable:
const useProject = async (projectId: Ref<string>, uniqueKey?: string) => {
// ...
const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, {
immediate: false,
key: uniqueKey || `project-${unref(projectId)}`
})
// ...
}
const useProject = async (projectId: Ref<string>, uniqueKey?: string) => {
// ...
const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, {
immediate: false,
key: uniqueKey || `project-${unref(projectId)}`
})
// ...
}
This way, each component can provide its own unique key when calling the composable, ensuring that the watchers work independently. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai5d ago
<@77852149421191168> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
hackal
hackalOP5d ago
@kapa.ai I see, but how come I see two requests in network and both finish with 200. Only one of the useFetch composables updates the data.
kapa.ai
kapa.ai5d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- You're observing an interesting behavior with useFetch that relates to how Nuxt handles data fetching and caching. Let me explain what's happening. When you see two network requests both completing with 200 status codes but only one instance of useFetch updating its data, this is likely due to how Nuxt's caching mechanism works with the composables. According to the documentation and issues in the knowledge sources: 1. When useFetch is used in multiple places with the same URL pattern but different request bodies, Nuxt may still consider them the same request if they don't have unique keys. This is because the body is not part of the auto-generated key calculation:
No, the body is not part of auto-generated keys, only query params, method, base url
GitHub Issue 2. Even though both requests are being made (as you see in the network tab), Nuxt's internal caching mechanism might be causing only one of the watchers to trigger. 3. A similar issue was reported in a Stack Overflow post where someone had multiple components using the same composable with useFetch, and the data was being overwritten:
When ChildPage also called the same function, it somehow overrode the result, which unfortunately wasn't what TopBar asked for.
Stack Overflow The solution is to ensure each useFetch call has a unique key. Even though you're using immediate: false, you still need to differentiate the cache keys:
const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, {
immediate: false,
key: `project-${unref(projectId)}-${uniqueComponentIdentifier}` // Add something unique per component
})
const {
data,
execute,
} = useFetch(() => `/my/url/${projectId}`, {
immediate: false,
key: `project-${unref(projectId)}-${uniqueComponentIdentifier}` // Add something unique per component
})
This ensures that each component has its own isolated cache entry, and both watchers will trigger independently when their respective data changes. Another approach mentioned in the knowledge sources is to use Promise.all to handle multiple fetch operations if they need to be coordinated:
Promise.all([fetchOperation1(), fetchOperation2()])
.then(([result1, result2]) => {
// Handle both results
})
Promise.all([fetchOperation1(), fetchOperation2()])
.then(([result1, result2]) => {
// Handle both results
})
This pattern was used in a Stack Overflow solution where multiple useFetch calls were causing issues. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai5d ago
<@77852149421191168> Kapa.ai is still learning and improving, please let me know how I did by reacting below.

Did you find this page helpful?