feat(nuxt): return status from useAsyncData (#21045)

This commit is contained in:
Nicolas Payot 2023-06-09 23:38:14 +02:00 committed by GitHub
parent 0505c9147d
commit c884a95f0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 78 additions and 1 deletions

View File

@ -38,11 +38,14 @@ type AsyncData<DataT, ErrorT> = {
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void> refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void> execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
error: Ref<ErrorT | null> error: Ref<ErrorT | null>
status: Ref<AsyncDataRequestStatus>
}; };
interface AsyncDataExecuteOptions { interface AsyncDataExecuteOptions {
dedupe?: boolean dedupe?: boolean
} }
type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
``` ```
## Params ## Params
@ -66,6 +69,7 @@ Under the hood, `lazy: false` uses `<Suspense>` to block the loading of the rout
* **pending**: a boolean indicating whether the data is still being fetched * **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 * **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 * **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. By default, Nuxt waits until a `refresh` is finished before it can be executed again.

View File

@ -5,6 +5,8 @@ import { useNuxtApp } from '../nuxt'
import { createError } from './error' import { createError } from './error'
import { onNuxtReady } from './ready' import { onNuxtReady } from './ready'
export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
export type _Transform<Input = any, Output = any> = (input: Input) => Output export type _Transform<Input = any, Output = any> = (input: Input) => Output
export type PickFrom<T, K extends Array<string>> = T extends Array<any> export type PickFrom<T, K extends Array<string>> = T extends Array<any>
@ -60,6 +62,7 @@ export interface _AsyncData<DataT, ErrorT> {
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void> refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void> execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
error: Ref<ErrorT | null> error: Ref<ErrorT | null>
status: Ref<AsyncDataRequestStatus>
} }
export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>> export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>>
@ -125,7 +128,8 @@ export function useAsyncData<
nuxt._asyncData[key] = { nuxt._asyncData[key] = {
data: ref(getCachedData() ?? options.default!()), data: ref(getCachedData() ?? options.default!()),
pending: ref(!hasCachedData()), 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 // TODO: Else, somehow check for conflicting keys with different defaults or fetcher
@ -144,6 +148,7 @@ export function useAsyncData<
return getCachedData() return getCachedData()
} }
asyncData.pending.value = true asyncData.pending.value = true
asyncData.status.value = 'pending'
// TODO: Cancel previous promise // TODO: Cancel previous promise
const promise = new Promise<ResT>( const promise = new Promise<ResT>(
(resolve, reject) => { (resolve, reject) => {
@ -166,6 +171,7 @@ export function useAsyncData<
} }
asyncData.data.value = result asyncData.data.value = result
asyncData.error.value = null asyncData.error.value = null
asyncData.status.value = 'success'
}) })
.catch((error: any) => { .catch((error: any) => {
// If this request is cancelled, resolve to the latest request. // If this request is cancelled, resolve to the latest request.
@ -173,6 +179,7 @@ export function useAsyncData<
asyncData.error.value = error asyncData.error.value = error
asyncData.data.value = unref(options.default!()) asyncData.data.value = unref(options.default!())
asyncData.status.value = 'error'
}) })
.finally(() => { .finally(() => {
if ((promise as any).cancelled) { return } if ((promise as any).cancelled) { return }
@ -222,6 +229,7 @@ export function useAsyncData<
if (fetchOnServer && nuxt.isHydrating && hasCachedData()) { if (fetchOnServer && nuxt.isHydrating && hasCachedData()) {
// 1. Hydration (server: true): no fetch // 1. Hydration (server: true): no fetch
asyncData.pending.value = false asyncData.pending.value = false
asyncData.status.value = asyncData.error.value ? 'error' : 'success'
} else if (instance && ((nuxt.payload.serverRendered && nuxt.isHydrating) || options.lazy) && options.immediate) { } else if (instance && ((nuxt.payload.serverRendered && nuxt.isHydrating) || options.lazy) && options.immediate) {
// 2. Initial load (server: false): fetch on mounted // 2. Initial load (server: false): fetch on mounted
// 3. Initial load or navigation (lazy: true): 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]!.data.value = undefined
nuxtApp._asyncData[key]!.error.value = undefined nuxtApp._asyncData[key]!.error.value = undefined
nuxtApp._asyncData[key]!.pending.value = false nuxtApp._asyncData[key]!.pending.value = false
nuxtApp._asyncData[key]!.status.value = 'idle'
} }
if (key in nuxtApp._asyncDataPromises) { if (key in nuxtApp._asyncDataPromises) {
nuxtApp._asyncDataPromises[key] = undefined nuxtApp._asyncDataPromises[key] = undefined

View File

@ -14,6 +14,7 @@ import type { RenderResponse } from 'nitropack'
import type { NuxtIslandContext } from '../core/runtime/nitro/renderer' import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
import type { RouteMiddleware } from '../../app' import type { RouteMiddleware } from '../../app'
import type { NuxtError } from '../app/composables/error' import type { NuxtError } from '../app/composables/error'
import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app') const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app')
@ -87,6 +88,7 @@ interface _NuxtApp {
data: Ref<any> data: Ref<any>
pending: Ref<boolean> pending: Ref<boolean>
error: Ref<any> error: Ref<any>
status: Ref<AsyncDataRequestStatus>
} | undefined> } | undefined>
/** @internal */ /** @internal */

View File

@ -1562,6 +1562,19 @@ describe.skipIf(isWindows)('useAsyncData', () => {
it('two requests made at once resolve and sync', async () => { it('two requests made at once resolve and sync', async () => {
await expectNoClientErrors('/useAsyncData/promise-all') 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', () => { describe.runIf(isDev())('component testing', () => {

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
const { status: status1 } = await useAsyncData(() => Promise.resolve(true))
if (status1.value !== 'success') {
throw new Error('status1 should be "success"')
}
const { status: status2 } = await useAsyncData(() => Promise.reject(Error('boom!')))
if (status2.value !== 'error') {
throw new Error('status2 should be "error"')
}
const { status: status3 } = await useAsyncData(() => Promise.resolve(true), { immediate: false })
if (status3.value !== 'idle') {
throw new Error('status3 should be "idle"')
}
const { status: status4, execute } = await useAsyncData(() => Promise.resolve(true), { immediate: false })
await execute()
if (status4.value !== 'success') {
throw new Error('status4 should be "success"')
}
const { status: status5 } = await useAsyncData(() => Promise.resolve(true), { server: false })
if (process.server && status5.value !== 'idle') {
throw new Error('status5 should be "idle" server side')
}
const status5Values = ref<string[]>([])
watchEffect(() => {
status5Values.value.push(status5.value)
})
</script>
<template>
<div>
Status
<div>
{{ status1 === 'success' }}
{{ status2 === 'error' }}
{{ status3 === 'idle' }}
{{ status4 === 'success' }}
<ClientOnly>
<div id="status5-values">
{{ status5Values.join(',') }}
</div>
</ClientOnly>
</div>
</div>
</template>