diff --git a/docs/3.api/2.composables/use-async-data.md b/docs/3.api/2.composables/use-async-data.md index 6195001cda..854c2bbf59 100644 --- a/docs/3.api/2.composables/use-async-data.md +++ b/docs/3.api/2.composables/use-async-data.md @@ -64,6 +64,9 @@ const { data: posts } = await useAsyncData( - `pick`: only pick specified keys in this array from the `handler` function result - `watch`: watch reactive sources to auto-refresh - `deep`: return data in a deep ref object (it is `true` by default). It can be set to `false` to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive. + - `dedupe`: avoid fetching same key more than once at a time (defaults to `cancel`). Possible options: + - `cancel` - cancels existing requests when a new one is made + - `defer` - does not make new requests at all if there is a pending request ::callout Under the hood, `lazy: false` uses `` to block the loading of the route before the data has been fetched. Consider using `lazy: true` and implementing a loading state instead for a snappier user experience. @@ -105,6 +108,7 @@ type AsyncDataOptions = { lazy?: boolean immediate?: boolean deep?: boolean + dedupe?: 'cancel' | 'defer' default?: () => DataT | Ref | null transform?: (input: DataT) => DataT pick?: string[] @@ -122,7 +126,7 @@ type AsyncData = { }; interface AsyncDataExecuteOptions { - dedupe?: boolean + dedupe?: 'cancel' | 'defer' } type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error' diff --git a/docs/3.api/2.composables/use-fetch.md b/docs/3.api/2.composables/use-fetch.md index b92f102fe5..d8b1349f53 100644 --- a/docs/3.api/2.composables/use-fetch.md +++ b/docs/3.api/2.composables/use-fetch.md @@ -96,6 +96,9 @@ All fetch options can be given a `computed` or `ref` value. These will be watche - `pick`: only pick specified keys in this array from the `handler` function result - `watch`: watch an array of reactive sources and auto-refresh the fetch result when they change. Fetch options and URL are watched by default. You can completely ignore reactive sources by using `watch: false`. Together with `immediate: false`, this allows for a fully-manual `useFetch`. - `deep`: return data in a deep ref object (it is `true` by default). It can be set to `false` to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive. + - `dedupe`: avoid fetching same key more than once at a time (defaults to `cancel`). Possible options: + - `cancel` - cancels existing requests when a new one is made + - `defer` - does not make new requests at all if there is a pending request ::callout If you provide a function or ref as the `url` parameter, or if you provide functions as arguments to the `options` parameter, then the `useFetch` call will not match other `useFetch` calls elsewhere in your codebase, even if the options seem to be identical. If you wish to force a match, you may provide your own key in `options`. @@ -136,6 +139,7 @@ type UseFetchOptions = { immediate?: boolean getCachedData?: (key: string) => DataT deep?: boolean + dedupe?: 'cancel' | 'defer' default?: () => DataT transform?: (input: DataT) => DataT pick?: string[] @@ -152,7 +156,7 @@ type AsyncData = { } interface AsyncDataExecuteOptions { - dedupe?: boolean + dedupe?: 'cancel' | 'defer' } type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error' diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 4702770445..b462e0d378 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -49,16 +49,21 @@ export interface AsyncDataOptions< watch?: MultiWatchSources immediate?: boolean deep?: boolean + dedupe?: 'cancel' | 'defer' } export interface AsyncDataExecuteOptions { _initial?: boolean + // TODO: deprecate boolean option in future minor /** * Force a refresh, even if there is already a pending request. Previous requests will * not be cancelled, but their result will not affect the data/pending state - and any * previously awaited promises will not resolve until this new request resolves. + * + * Instead of using `boolean` values, use `cancel` for `true` and `defer` for `false`. + * Boolean values will be removed in a future release. */ - dedupe?: boolean + dedupe?: boolean | 'cancel' | 'defer' } export interface _AsyncData { @@ -72,6 +77,9 @@ export interface _AsyncData { export type AsyncData = _AsyncData & Promise<_AsyncData> +// TODO: deprecate boolean option in future minor +const isDefer = (dedupe?: boolean | 'cancel' | 'defer') => dedupe === 'defer' || dedupe === false + export function useAsyncData< ResT, DataE = Error, @@ -150,6 +158,7 @@ export function useAsyncData< options.lazy = options.lazy ?? false options.immediate = options.immediate ?? true options.deep = options.deep ?? asyncDataDefaults.deep + options.dedupe = options.dedupe ?? 'cancel' const hasCachedData = () => ![null, undefined].includes(options.getCachedData!(key) as any) @@ -172,7 +181,7 @@ export function useAsyncData< asyncData.refresh = asyncData.execute = (opts = {}) => { if (nuxt._asyncDataPromises[key]) { - if (opts.dedupe === false) { + if (isDefer(opts.dedupe ?? options.dedupe)) { // Avoid fetching same key more than once at a time return nuxt._asyncDataPromises[key]! } diff --git a/test/nuxt/composables.test.ts b/test/nuxt/composables.test.ts index 4daa1b3270..43f6ef2dea 100644 --- a/test/nuxt/composables.test.ts +++ b/test/nuxt/composables.test.ts @@ -239,6 +239,33 @@ describe('useAsyncData', () => { const { data } = await useAsyncData(() => Promise.reject(new Error('test')), { default: () => 'default' }) expect(data.value).toMatchInlineSnapshot('"default"') }) + + it('should execute the promise function once when dedupe option is "defer" for multiple calls', async () => { + const promiseFn = vi.fn(() => Promise.resolve('test')) + useAsyncData('dedupedKey', promiseFn, { dedupe: 'defer' }) + useAsyncData('dedupedKey', promiseFn, { dedupe: 'defer' }) + useAsyncData('dedupedKey', promiseFn, { dedupe: 'defer' }) + + expect(promiseFn).toHaveBeenCalledTimes(1) + }) + + it('should execute the promise function multiple times when dedupe option is not specified for multiple calls', async () => { + const promiseFn = vi.fn(() => Promise.resolve('test')) + useAsyncData('dedupedKey', promiseFn) + useAsyncData('dedupedKey', promiseFn) + useAsyncData('dedupedKey', promiseFn) + + expect(promiseFn).toHaveBeenCalledTimes(3) + }) + + it('should execute the promise function as per dedupe option when different dedupe options are used for multiple calls', async () => { + const promiseFn = vi.fn(() => Promise.resolve('test')) + useAsyncData('dedupedKey', promiseFn, { dedupe: 'defer' }) + useAsyncData('dedupedKey', promiseFn) + useAsyncData('dedupedKey', promiseFn, { dedupe: 'defer' }) + + expect(promiseFn).toHaveBeenCalledTimes(2) + }) }) describe('useFetch', () => {