diff --git a/docs/content/1.getting-started/6.data-fetching.md b/docs/content/1.getting-started/6.data-fetching.md index a34b2624c2..5bc3086b6f 100644 --- a/docs/content/1.getting-started/6.data-fetching.md +++ b/docs/content/1.getting-started/6.data-fetching.md @@ -152,6 +152,12 @@ function next() { The key to making this work is to call the `refresh()` method returned from the `useFetch()` composable when a query parameter has changed. +By default, `refresh()` will not make a new request if one is already pending. You can override any pending requests with the override option. Previous requests will not be cancelled, but their result will not update the data or pending state - and any previously awaited promises will not resolve until this new request resolves. + +```js +refresh({ override: true }) +``` + ### `refreshNuxtData` Invalidate the cache of `useAsyncData`, `useLazyAsyncData`, `useFetch` and `useLazyFetch` and trigger the refetch. diff --git a/docs/content/3.api/1.composables/use-async-data.md b/docs/content/3.api/1.composables/use-async-data.md index c462022791..bdb6a8c1db 100644 --- a/docs/content/3.api/1.composables/use-async-data.md +++ b/docs/content/3.api/1.composables/use-async-data.md @@ -30,7 +30,7 @@ type AsyncDataOptions = { } interface RefreshOptions { - _initial?: boolean + override?: boolean } type AsyncData = { diff --git a/docs/content/3.api/1.composables/use-fetch.md b/docs/content/3.api/1.composables/use-fetch.md index 8d5e296c5d..a40aafba38 100644 --- a/docs/content/3.api/1.composables/use-fetch.md +++ b/docs/content/3.api/1.composables/use-fetch.md @@ -32,7 +32,7 @@ type UseFetchOptions = { type AsyncData = { data: Ref pending: Ref - refresh: () => Promise + refresh: (opts?: { override?: boolean }) => Promise execute: () => Promise error: Ref } diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 51a338619e..5fe425d11c 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -34,6 +34,12 @@ export interface AsyncDataOptions< export interface AsyncDataExecuteOptions { _initial?: boolean + /** + * 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. + */ + override?: boolean } export interface _AsyncData { @@ -115,9 +121,12 @@ export function useAsyncData< const asyncData = { ...nuxt._asyncData[key] } as AsyncData asyncData.refresh = asyncData.execute = (opts = {}) => { - // Avoid fetching same key more than once at a time if (nuxt._asyncDataPromises[key]) { - return nuxt._asyncDataPromises[key] + if (!opts.override) { + // Avoid fetching same key more than once at a time + return nuxt._asyncDataPromises[key] + } + (nuxt._asyncDataPromises[key] as any).cancelled = true } // Avoid fetching same key that is already fetched if (opts._initial && useInitialCache()) { @@ -125,7 +134,7 @@ export function useAsyncData< } asyncData.pending.value = true // TODO: Cancel previous promise - nuxt._asyncDataPromises[key] = new Promise( + const promise = new Promise( (resolve, reject) => { try { resolve(handler(nuxt)) @@ -134,6 +143,9 @@ export function useAsyncData< } }) .then((result) => { + // If this request is cancelled, resolve to the latest request. + if ((promise as any).cancelled) { return nuxt._asyncDataPromises[key] } + if (options.transform) { result = options.transform(result) } @@ -144,10 +156,15 @@ export function useAsyncData< asyncData.error.value = null }) .catch((error: any) => { + // If this request is cancelled, resolve to the latest request. + if ((promise as any).cancelled) { return nuxt._asyncDataPromises[key] } + asyncData.error.value = error asyncData.data.value = unref(options.default?.() ?? null) }) .finally(() => { + if ((promise as any).cancelled) { return } + asyncData.pending.value = false nuxt.payload.data[key] = asyncData.data.value if (asyncData.error.value) { @@ -155,6 +172,7 @@ export function useAsyncData< } delete nuxt._asyncDataPromises[key] }) + nuxt._asyncDataPromises[key] = promise return nuxt._asyncDataPromises[key] } diff --git a/packages/nuxt/src/app/composables/fetch.ts b/packages/nuxt/src/app/composables/fetch.ts index 9b2b0294d1..229617c9d0 100644 --- a/packages/nuxt/src/app/composables/fetch.ts +++ b/packages/nuxt/src/app/composables/fetch.ts @@ -86,8 +86,12 @@ export function useFetch< ] } + let controller: AbortController + const asyncData = useAsyncData<_ResT, ErrorT, Transform, PickKeys>(key, () => { - return $fetch(_request.value, _fetchOptions) as Promise<_ResT> + controller?.abort?.() + controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController + return $fetch(_request.value, { signal: controller.signal, ..._fetchOptions }) as Promise<_ResT> }, _asyncDataOptions) return asyncData diff --git a/test/basic.test.ts b/test/basic.test.ts index 3a40675be7..c288fc1244 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -824,6 +824,10 @@ describe.skipIf(isWindows)('useAsyncData', () => { await $fetch('/useAsyncData/refresh') }) + it('requests can be cancelled/overridden', async () => { + await expectNoClientErrors('/useAsyncData/override') + }) + it('two requests made at once resolve and sync', async () => { await expectNoClientErrors('/useAsyncData/promise-all') }) diff --git a/test/fixtures/basic/pages/useAsyncData/override.vue b/test/fixtures/basic/pages/useAsyncData/override.vue new file mode 100644 index 0000000000..82d6986673 --- /dev/null +++ b/test/fixtures/basic/pages/useAsyncData/override.vue @@ -0,0 +1,36 @@ + + +