diff --git a/docs/content/1.getting-started/6.data-fetching.md b/docs/content/1.getting-started/6.data-fetching.md index 03c5cee2b75..490590992a9 100644 --- a/docs/content/1.getting-started/6.data-fetching.md +++ b/docs/content/1.getting-started/6.data-fetching.md @@ -147,6 +147,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 6093917cc8e..4d6a33b48d2 100644 --- a/docs/content/3.api/1.composables/use-async-data.md +++ b/docs/content/3.api/1.composables/use-async-data.md @@ -27,7 +27,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 c44b6e4f258..52a53dad601 100644 --- a/docs/content/3.api/1.composables/use-fetch.md +++ b/docs/content/3.api/1.composables/use-fetch.md @@ -30,7 +30,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 51a338619e3..5fe425d11ca 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 9b2b0294d10..229617c9d01 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 2fe9c019dba..31c722e5807 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -670,6 +670,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 00000000000..82d69866733 --- /dev/null +++ b/test/fixtures/basic/pages/useAsyncData/override.vue @@ -0,0 +1,36 @@ + + +