Skip to content

Commit 724e1a6

Browse files
authored
fix(nuxt): use single synced asyncdata instance per key (#31373)
1 parent 31b46e3 commit 724e1a6

File tree

16 files changed

+841
-217
lines changed

16 files changed

+841
-217
lines changed

docs/1.getting-started/10.data-fetching.md

+59-3
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ In the example above, `useFetch` would make sure that the request would occur in
5656

5757
### Suspense
5858

59-
Nuxt uses Vues [`<Suspense>`](https://vuejs.org/guide/built-ins/suspense) component under the hood to prevent navigation before every async data is available to the view. The data fetching composables can help you leverage this feature and use what suits best on a per-call basis.
59+
Nuxt uses Vue's [`<Suspense>`](https://vuejs.org/guide/built-ins/suspense) component under the hood to prevent navigation before every async data is available to the view. The data fetching composables can help you leverage this feature and use what suits best on a per-call basis.
6060

6161
::note
6262
You can add the [`<NuxtLoadingIndicator>`](/docs/api/components/nuxt-loading-indicator) to add a progress bar between page navigations.
@@ -250,7 +250,7 @@ If you have not fetched data on the server (for example, with `server: false`),
250250

251251
### Lazy
252252

253-
By default, data fetching composables will wait for the resolution of their asynchronous function before navigating to a new page by using Vues Suspense. This feature can be ignored on client-side navigation with the `lazy` option. In that case, you will have to manually handle loading state using the `status` value.
253+
By default, data fetching composables will wait for the resolution of their asynchronous function before navigating to a new page by using Vue's Suspense. This feature can be ignored on client-side navigation with the `lazy` option. In that case, you will have to manually handle loading state using the `status` value.
254254

255255
```vue twoslash [app.vue]
256256
<script setup lang="ts">
@@ -350,14 +350,70 @@ Both `pick` and `transform` don't prevent the unwanted data from being fetched i
350350
[`useFetch`](/docs/api/composables/use-fetch) and [`useAsyncData`](/docs/api/composables/use-async-data) use keys to prevent refetching the same data.
351351

352352
- [`useFetch`](/docs/api/composables/use-fetch) uses the provided URL as a key. Alternatively, a `key` value can be provided in the `options` object passed as a last argument.
353-
- [`useAsyncData`](/docs/api/composables/use-async-data) uses its first argument as a key if it is a string. If the first argument is the handler function that performs the query, then a key that is unique to the file name and line number of the instance of `useAsyncData` will be generated for you.
353+
- [`useAsyncData`](/docs/api/composables/use-async-data) uses its first argument as a key if it is a string. If the first argument is the handler function that performs the query, then a key that is unique to the file name and line number of the instance of `useAsyncData` will be generated for you.
354354

355355
::tip
356356
To get the cached data by key, you can use [`useNuxtData`](/docs/api/composables/use-nuxt-data)
357357
::
358358

359359
:video-accordion{title="Watch a video from Vue School on caching data with the key option" videoId="1026410044" platform="vimeo"}
360360

361+
#### Shared State and Option Consistency
362+
363+
When multiple components use the same key with `useAsyncData` or `useFetch`, they will share the same `data`, `error` and `status` refs. This ensures consistency across components but requires some options to be consistent.
364+
365+
The following options **must be consistent** across all calls with the same key:
366+
- `handler` function
367+
- `deep` option
368+
- `transform` function
369+
- `pick` array
370+
- `getCachedData` function
371+
- `default` value
372+
373+
```ts
374+
// ❌ This will trigger a development warning
375+
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
376+
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
377+
```
378+
379+
The following options **can safely differ** without triggering warnings:
380+
- `server`
381+
- `lazy`
382+
- `immediate`
383+
- `dedupe`
384+
- `watch`
385+
386+
```ts
387+
// ✅ This is allowed
388+
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: true })
389+
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: false })
390+
```
391+
392+
If you need independent instances, use different keys:
393+
394+
```ts
395+
// These are completely independent instances
396+
const { data: users1 } = useAsyncData('users-1', () => $fetch('/api/users'))
397+
const { data: users2 } = useAsyncData('users-2', () => $fetch('/api/users'))
398+
```
399+
400+
#### Reactive Keys
401+
402+
You can use computed refs, plain refs or getter functions as keys, allowing for dynamic data fetching that automatically updates when dependencies change:
403+
404+
```ts
405+
// Using a computed property as a key
406+
const userId = ref('123')
407+
const { data: user } = useAsyncData(
408+
computed(() => `user-${userId.value}`),
409+
() => fetchUser(userId.value)
410+
)
411+
412+
// When userId changes, the data will be automatically refetched
413+
// and the old data will be cleaned up if no other components use it
414+
userId.value = '456'
415+
```
416+
361417
#### Refresh and execute
362418

363419
If you want to fetch or refresh data manually, use the `execute` or `refresh` function provided by the composables.

docs/1.getting-started/18.upgrade.md

+74
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,80 @@ export default defineNuxtConfig({
225225
})
226226
```
227227

228+
### Singleton Data Fetching Layer
229+
230+
🚦 **Impact Level**: Moderate
231+
232+
#### What Changed
233+
234+
Nuxt's data fetching system (`useAsyncData` and `useFetch`) has been significantly reorganized for better performance and consistency:
235+
236+
1. **Shared refs for the same key**: All calls to `useAsyncData` or `useFetch` with the same key now share the same `data`, `error` and `status` refs. This means that it is important that all calls with an explicit key must not have conflicting `deep`, `transform`, `pick`, `getCachedData` or `default` options.
237+
238+
2. **More control over `getCachedData`**: The `getCachedData` function is now called every time data is fetched, even if this is caused by a watcher or calling `refreshNuxtData`. (Previously, new data was always fetched and this function was not called in these cases.) To allow more control over when to use cached data and when to refetch, the function now receives a context object with the cause of the request.
239+
240+
3. **Reactive key support**: You can now use computed refs, plain refs or getter functions as keys, which enables automatic data refetching (and stores data separately).
241+
242+
4. **Data cleanup**: When the last component using data fetched with `useAsyncData` is unmounted, Nuxt will remove that data to avoid ever-growing memory usage.
243+
244+
#### Reasons for Change
245+
246+
These changes have been made to improve memory usage and increase consistency with loading states across calls of `useAsyncData`.
247+
248+
#### Migration Steps
249+
250+
1. **Check for inconsistent options**: Review any components using the same key with different options or fetch functions.
251+
252+
```ts
253+
// This will now trigger a warning
254+
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
255+
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
256+
```
257+
258+
It may be beneficial to extract any calls to `useAsyncData` that share an explicit key (and have custom options) into their own composable:
259+
260+
```ts [composables/useUserData.ts]
261+
export function useUserData(userId: string) {
262+
return useAsyncData(
263+
`user-${userId}`,
264+
() => fetchUser(userId),
265+
{
266+
deep: true,
267+
transform: (user) => ({ ...user, lastAccessed: new Date() })
268+
}
269+
)
270+
}
271+
```
272+
273+
2. **Update `getCachedData` implementations**:
274+
275+
```diff
276+
useAsyncData('key', fetchFunction, {
277+
- getCachedData: (key, nuxtApp) => {
278+
- return cachedData[key]
279+
- }
280+
+ getCachedData: (key, nuxtApp, ctx) => {
281+
+ // ctx.cause - can be 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch'
282+
+
283+
+ // Example: Don't use cache on manual refresh
284+
+ if (ctx.cause === 'refresh:manual') return undefined
285+
+
286+
+ return cachedData[key]
287+
+ }
288+
})
289+
```
290+
291+
Alternatively, for now, you can disable this behaviour with:
292+
293+
```ts twoslash [nuxt.config.ts]
294+
export default defineNuxtConfig({
295+
experimental: {
296+
granularCachedData: false,
297+
purgeCachedData: false
298+
}
299+
})
300+
```
301+
228302
### Deduplication of Route Metadata
229303

230304
🚦 **Impact Level**: Minimal

docs/2.guide/3.going-further/1.experimental-features.md

+16
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,19 @@ export default defineNuxtConfig({
605605
::read-more{icon="i-simple-icons-github" color="gray" to="https://github.com/nuxt/nuxt/pull/31379" target="_blank"}
606606
See PR #31379 for implementation details.
607607
::
608+
609+
## granularCachedData
610+
611+
Whether to call and use the result from `getCachedData` when refreshing data for `useAsyncData` and `useFetch` (whether by `watch`, `refreshNuxtData()`, or a manual `refresh()` call.
612+
613+
```ts twoslash [nuxt.config.ts]
614+
export default defineNuxtConfig({
615+
experimental: {
616+
granularCachedData: true
617+
}
618+
})
619+
```
620+
621+
::read-more{icon="i-simple-icons-github" color="gray" to="https://github.com/nuxt/nuxt/pull/31373" target="_blank"}
622+
See PR #31373 for implementation details.
623+
::

docs/3.api/2.composables/use-async-data.md

+56-5
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,25 @@ const { data: posts } = await useAsyncData(
5353
</script>
5454
```
5555

56+
### Reactive Keys
57+
58+
You can use a computed ref, plain ref or a getter function as the key, allowing for dynamic data fetching that automatically updates when the key changes:
59+
60+
```vue [pages/[id\\].vue]
61+
<script setup lang="ts">
62+
const route = useRoute()
63+
const userId = computed(() => `user-${route.params.id}`)
64+
65+
// When the route changes and userId updates, the data will be automatically refetched
66+
const { data: user } = useAsyncData(
67+
userId,
68+
() => fetchUserById(route.params.id)
69+
)
70+
</script>
71+
```
72+
5673
::warning
57-
[`useAsyncData`](/docs/api/composables/use-async-data) is a reserved function name transformed by the compiler, so you should not name your own function [`useAsyncData`](/docs/api/composables/use-async-data) .
74+
[`useAsyncData`](/docs/api/composables/use-async-data) is a reserved function name transformed by the compiler, so you should not name your own function [`useAsyncData`](/docs/api/composables/use-async-data).
5875
::
5976

6077
:read-more{to="/docs/getting-started/data-fetching#useasyncdata"}
@@ -74,7 +91,7 @@ The `handler` function should be **side-effect free** to ensure predictable beha
7491
- `transform`: a function that can be used to alter `handler` function result after resolving
7592
- `getCachedData`: Provide a function which returns cached data. A `null` or `undefined` return value will trigger a fetch. By default, this is:
7693
```ts
77-
const getDefaultCachedData = (key, nuxtApp) => nuxtApp.isHydrating
94+
const getDefaultCachedData = (key, nuxtApp, ctx) => nuxtApp.isHydrating
7895
? nuxtApp.payload.data[key]
7996
: nuxtApp.static.data[key]
8097
```
@@ -96,6 +113,35 @@ You can use `useLazyAsyncData` to have the same behavior as `lazy: true` with `u
96113

97114
:video-accordion{title="Watch a video from Alexander Lichter about client-side caching with getCachedData" videoId="aQPR0xn-MMk"}
98115

116+
### Shared State and Option Consistency
117+
118+
When using the same key for multiple `useAsyncData` calls, they will share the same `data`, `error` and `status` refs. This ensures consistency across components but requires option consistency.
119+
120+
The following options **must be consistent** across all calls with the same key:
121+
- `handler` function
122+
- `deep` option
123+
- `transform` function
124+
- `pick` array
125+
- `getCachedData` function
126+
- `default` value
127+
128+
The following options **can differ** without triggering warnings:
129+
- `server`
130+
- `lazy`
131+
- `immediate`
132+
- `dedupe`
133+
- `watch`
134+
135+
```ts
136+
// ❌ This will trigger a development warning
137+
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
138+
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
139+
140+
// ✅ This is allowed
141+
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: true })
142+
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: false })
143+
```
144+
99145
## Return Values
100146

101147
- `data`: the result of the asynchronous function that is passed in.
@@ -124,7 +170,7 @@ function useAsyncData<DataT, DataE>(
124170
options?: AsyncDataOptions<DataT>
125171
): AsyncData<DataT, DataE>
126172
function useAsyncData<DataT, DataE>(
127-
key: string,
173+
key: string | Ref<string> | ComputedRef<string>,
128174
handler: (nuxtApp?: NuxtApp) => Promise<DataT>,
129175
options?: AsyncDataOptions<DataT>
130176
): Promise<AsyncData<DataT, DataE>>
@@ -138,8 +184,13 @@ type AsyncDataOptions<DataT> = {
138184
default?: () => DataT | Ref<DataT> | null
139185
transform?: (input: DataT) => DataT | Promise<DataT>
140186
pick?: string[]
141-
watch?: WatchSource[]
142-
getCachedData?: (key: string, nuxtApp: NuxtApp) => DataT | undefined
187+
watch?: WatchSource[] | false
188+
getCachedData?: (key: string, nuxtApp: NuxtApp, ctx: AsyncDataRequestContext) => DataT | undefined
189+
}
190+
191+
type AsyncDataRequestContext = {
192+
/** The reason for this data request */
193+
cause: 'initial' | 'refresh:manual' | 'refresh:hook' | 'watch'
143194
}
144195

145196
type AsyncData<DataT, ErrorT> = {

docs/3.api/2.composables/use-fetch.md

+23-2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,22 @@ const { data, status, error, refresh, clear } = await useFetch('/api/auth/login'
6666
})
6767
```
6868

69+
### Reactive Keys and Shared State
70+
71+
You can use a computed ref or a plain ref as the URL, allowing for dynamic data fetching that automatically updates when the URL changes:
72+
73+
```vue [pages/[id\\].vue]
74+
<script setup lang="ts">
75+
const route = useRoute()
76+
const id = computed(() => route.params.id)
77+
78+
// When the route changes and id updates, the data will be automatically refetched
79+
const { data: post } = await useFetch(() => `/api/posts/${id.value}`)
80+
</script>
81+
```
82+
83+
When using `useFetch` with the same URL and options in multiple components, they will share the same `data`, `error` and `status` refs. This ensures consistency across components.
84+
6985
::warning
7086
`useFetch` is a reserved function name transformed by the compiler, so you should not name your own function `useFetch`.
7187
::
@@ -109,7 +125,7 @@ All fetch options can be given a `computed` or `ref` value. These will be watche
109125
- `transform`: a function that can be used to alter `handler` function result after resolving
110126
- `getCachedData`: Provide a function which returns cached data. A `null` or `undefined` return value will trigger a fetch. By default, this is:
111127
```ts
112-
const getDefaultCachedData = (key, nuxtApp) => nuxtApp.isHydrating
128+
const getDefaultCachedData = (key, nuxtApp, ctx) => nuxtApp.isHydrating
113129
? nuxtApp.payload.data[key]
114130
: nuxtApp.static.data[key]
115131
```
@@ -170,7 +186,7 @@ type UseFetchOptions<DataT> = {
170186
server?: boolean
171187
lazy?: boolean
172188
immediate?: boolean
173-
getCachedData?: (key: string, nuxtApp: NuxtApp) => DataT | undefined
189+
getCachedData?: (key: string, nuxtApp: NuxtApp, ctx: AsyncDataRequestContext) => DataT | undefined
174190
deep?: boolean
175191
dedupe?: 'cancel' | 'defer'
176192
default?: () => DataT
@@ -179,6 +195,11 @@ type UseFetchOptions<DataT> = {
179195
watch?: WatchSource[] | false
180196
}
181197

198+
type AsyncDataRequestContext = {
199+
/** The reason for this data request */
200+
cause: 'initial' | 'refresh:manual' | 'refresh:hook' | 'watch'
201+
}
202+
182203
type AsyncData<DataT, ErrorT> = {
183204
data: Ref<DataT | null>
184205
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>

nuxt.config.ts

+13
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ export default defineNuxtConfig({
2121
dir: {
2222
app: fileURLToPath(new URL('./test/runtime/app', import.meta.url)),
2323
},
24+
vite: {
25+
plugins: [
26+
{
27+
name: 'enable some dev logging',
28+
enforce: 'pre',
29+
transform (code) {
30+
if (code.includes('import.meta.dev /* and in test */')) {
31+
return code.replace('import.meta.dev /* and in test */', 'true')
32+
}
33+
},
34+
},
35+
],
36+
},
2437
typescript: {
2538
shim: process.env.DOCS_TYPECHECK === 'true',
2639
hoist: ['@vitejs/plugin-vue', 'vue-router'],

0 commit comments

Comments
 (0)