From c884a95f0f7477e92ea50dc3910cbbe3e8e5ec5b Mon Sep 17 00:00:00 2001 From: Nicolas Payot Date: Fri, 9 Jun 2023 23:38:14 +0200 Subject: [PATCH] feat(nuxt): return `status` from `useAsyncData` (#21045) --- docs/3.api/1.composables/use-async-data.md | 4 ++ .../nuxt/src/app/composables/asyncData.ts | 11 ++++- packages/nuxt/src/app/nuxt.ts | 2 + test/basic.test.ts | 13 +++++ .../basic/pages/useAsyncData/status.vue | 49 +++++++++++++++++++ 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/basic/pages/useAsyncData/status.vue diff --git a/docs/3.api/1.composables/use-async-data.md b/docs/3.api/1.composables/use-async-data.md index 7b04f47b64..3a43be726e 100644 --- a/docs/3.api/1.composables/use-async-data.md +++ b/docs/3.api/1.composables/use-async-data.md @@ -38,11 +38,14 @@ type AsyncData = { refresh: (opts?: AsyncDataExecuteOptions) => Promise execute: (opts?: AsyncDataExecuteOptions) => Promise error: Ref + status: Ref }; interface AsyncDataExecuteOptions { dedupe?: boolean } + +type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error' ``` ## Params @@ -66,6 +69,7 @@ Under the hood, `lazy: false` uses `` to block the loading of the rout * **pending**: a boolean indicating whether the data is still being fetched * **refresh**/**execute**: a function that can be used to refresh the data returned by the `handler` function * **error**: an error object if the data fetching failed +* **status**: a string indicating the status of the data request (`"idle"`, `"pending"`, `"success"`, `"error"`) By default, Nuxt waits until a `refresh` is finished before it can be executed again. diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index dedc712926..9cfad40dd4 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -5,6 +5,8 @@ import { useNuxtApp } from '../nuxt' import { createError } from './error' import { onNuxtReady } from './ready' +export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error' + export type _Transform = (input: Input) => Output export type PickFrom> = T extends Array @@ -60,6 +62,7 @@ export interface _AsyncData { refresh: (opts?: AsyncDataExecuteOptions) => Promise execute: (opts?: AsyncDataExecuteOptions) => Promise error: Ref + status: Ref } export type AsyncData = _AsyncData & Promise<_AsyncData> @@ -125,7 +128,8 @@ export function useAsyncData< nuxt._asyncData[key] = { data: ref(getCachedData() ?? options.default!()), pending: ref(!hasCachedData()), - error: toRef(nuxt.payload._errors, key) + error: toRef(nuxt.payload._errors, key), + status: ref('idle') } } // TODO: Else, somehow check for conflicting keys with different defaults or fetcher @@ -144,6 +148,7 @@ export function useAsyncData< return getCachedData() } asyncData.pending.value = true + asyncData.status.value = 'pending' // TODO: Cancel previous promise const promise = new Promise( (resolve, reject) => { @@ -166,6 +171,7 @@ export function useAsyncData< } asyncData.data.value = result asyncData.error.value = null + asyncData.status.value = 'success' }) .catch((error: any) => { // If this request is cancelled, resolve to the latest request. @@ -173,6 +179,7 @@ export function useAsyncData< asyncData.error.value = error asyncData.data.value = unref(options.default!()) + asyncData.status.value = 'error' }) .finally(() => { if ((promise as any).cancelled) { return } @@ -222,6 +229,7 @@ export function useAsyncData< if (fetchOnServer && nuxt.isHydrating && hasCachedData()) { // 1. Hydration (server: true): no fetch asyncData.pending.value = false + asyncData.status.value = asyncData.error.value ? 'error' : 'success' } else if (instance && ((nuxt.payload.serverRendered && nuxt.isHydrating) || options.lazy) && options.immediate) { // 2. Initial load (server: false): fetch on mounted // 3. Initial load or navigation (lazy: true): fetch on mounted @@ -328,6 +336,7 @@ export function clearNuxtData (keys?: string | string[] | ((key: string) => bool nuxtApp._asyncData[key]!.data.value = undefined nuxtApp._asyncData[key]!.error.value = undefined nuxtApp._asyncData[key]!.pending.value = false + nuxtApp._asyncData[key]!.status.value = 'idle' } if (key in nuxtApp._asyncDataPromises) { nuxtApp._asyncDataPromises[key] = undefined diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 52e3be7015..c705912c84 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -14,6 +14,7 @@ import type { RenderResponse } from 'nitropack' import type { NuxtIslandContext } from '../core/runtime/nitro/renderer' import type { RouteMiddleware } from '../../app' import type { NuxtError } from '../app/composables/error' +import type { AsyncDataRequestStatus } from '../app/composables/asyncData' const nuxtAppCtx = /* #__PURE__ */ getContext('nuxt-app') @@ -87,6 +88,7 @@ interface _NuxtApp { data: Ref pending: Ref error: Ref + status: Ref } | undefined> /** @internal */ diff --git a/test/basic.test.ts b/test/basic.test.ts index 8f274c2a64..01a320ada1 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -1562,6 +1562,19 @@ describe.skipIf(isWindows)('useAsyncData', () => { it('two requests made at once resolve and sync', async () => { await expectNoClientErrors('/useAsyncData/promise-all') }) + + it('requests status can be used', async () => { + const html = await $fetch('/useAsyncData/status') + expect(html).toContain('true') + expect(html).not.toContain('false') + + const page = await createPage('/useAsyncData/status') + await page.waitForLoadState('networkidle') + + expect(await page.locator('#status5-values').textContent()).toContain('idle,pending,success') + + await page.close() + }) }) describe.runIf(isDev())('component testing', () => { diff --git a/test/fixtures/basic/pages/useAsyncData/status.vue b/test/fixtures/basic/pages/useAsyncData/status.vue new file mode 100644 index 0000000000..e1ca04e4fc --- /dev/null +++ b/test/fixtures/basic/pages/useAsyncData/status.vue @@ -0,0 +1,49 @@ + + +