From 385674494d816873b567e5fe3a93726bbd906456 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 10 Oct 2022 11:33:16 +0100 Subject: [PATCH] feat(nuxt): refresh override for data fetching composables (#7864) --- .../1.getting-started/6.data-fetching.md | 6 ++++ .../3.api/1.composables/use-async-data.md | 2 +- docs/content/3.api/1.composables/use-fetch.md | 2 +- .../nuxt/src/app/composables/asyncData.ts | 24 +++++++++++-- packages/nuxt/src/app/composables/fetch.ts | 6 +++- test/basic.test.ts | 4 +++ .../basic/pages/useAsyncData/override.vue | 36 +++++++++++++++++++ 7 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/basic/pages/useAsyncData/override.vue diff --git a/docs/content/1.getting-started/6.data-fetching.md b/docs/content/1.getting-started/6.data-fetching.md index a34b2624c26..5bc3086b6f3 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 c462022791f..bdb6a8c1dbf 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 8d5e296c5dc..a40aafba38a 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 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 3a40675be7d..c288fc1244c 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 00000000000..82d69866733 --- /dev/null +++ b/test/fixtures/basic/pages/useAsyncData/override.vue @@ -0,0 +1,36 @@ + + +