fix(nuxt): use undefined rather than null for data fetching defaults (#27294)

This commit is contained in:
Daniel Roe 2024-05-21 23:58:38 +01:00 committed by GitHub
parent a531e6f466
commit 8a759bc6fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 188 additions and 104 deletions

View File

@ -189,6 +189,37 @@ export default defineNuxtConfig({
})
```
#### Default `data` and `error` values in `useAsyncData` and `useFetch`
🚦 **Impact Level**: Minimal
##### What Changed
`data` and `error` objects returned from `useAsyncData` will now default to `undefined`.
##### Reasons for Change
Previously `data` was initialized to `null` but reset in `clearNuxtData` to `undefined`. `error` was initialized to `null`. This change is to bring greater consistency.
##### Migration Steps
If you encounter any issues you can revert back to the previous behavior with:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
defaults: {
useAsyncData: {
value: 'null',
errorValue: 'null'
}
}
}
})
```
Please report an issue if you are doing this, as we do not plan to keep this as configurable.
#### Shallow Data Reactivity in `useAsyncData` and `useFetch`
🚦 **Impact Level**: Minimal

View File

@ -19,4 +19,10 @@ declare global {
}
}
declare module '#app/defaults' {
type DefaultAsyncDataErrorValue = undefined
type DefaultAsyncDataValue = undefined
type DefaultErrorValue = undefined
}
export {}

View File

@ -10,6 +10,9 @@ import { onNuxtReady } from './ready'
// @ts-expect-error virtual file
import { asyncDataDefaults } from '#build/nuxt.config.mjs'
// TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
export type _Transform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>
@ -42,7 +45,7 @@ export interface AsyncDataOptions<
ResT,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> {
/**
* Whether to fetch on the server side.
@ -117,7 +120,7 @@ export interface _AsyncData<DataT, ErrorT> {
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
clear: () => void
error: Ref<ErrorT | null>
error: Ref<ErrorT | DefaultAsyncDataErrorValue>
status: Ref<AsyncDataRequestStatus>
}
@ -138,11 +141,11 @@ export function useAsyncData<
NuxtErrorDataT = unknown,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
/**
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
@ -158,7 +161,7 @@ export function useAsyncData<
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
/**
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
@ -171,12 +174,12 @@ export function useAsyncData<
NuxtErrorDataT = unknown,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
/**
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
@ -194,14 +197,14 @@ export function useAsyncData<
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
export function useAsyncData<
ResT,
NuxtErrorDataT = unknown,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null> {
DefaultT = DefaultAsyncDataValue,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue> {
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
@ -233,7 +236,7 @@ export function useAsyncData<
}
// Used to get default values
const getDefault = () => null
const getDefault = () => asyncDataDefaults.value
const getDefaultCachedData = () => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key]
// Apply defaults
@ -250,11 +253,12 @@ export function useAsyncData<
console.warn('[nuxt] `boolean` values are deprecated for the `dedupe` option of `useAsyncData` and will be removed in the future. Use \'cancel\' or \'defer\' instead.')
}
// TODO: make more precise when v4 lands
const hasCachedData = () => options.getCachedData!(key, nuxtApp) != null
// Create or use a shared asyncData entity
if (!nuxtApp._asyncData[key] || !options.immediate) {
nuxtApp.payload._errors[key] ??= null
nuxtApp.payload._errors[key] ??= asyncDataDefaults.errorValue
const _ref = options.deep ? ref : shallowRef
@ -307,7 +311,7 @@ export function useAsyncData<
nuxtApp.payload.data[key] = result
asyncData.data.value = result
asyncData.error.value = null
asyncData.error.value = asyncDataDefaults.errorValue
asyncData.status.value = 'success'
})
.catch((error: any) => {
@ -404,11 +408,11 @@ export function useLazyAsyncData<
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
@ -418,18 +422,18 @@ export function useLazyAsyncData<
> (
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
@ -440,15 +444,15 @@ export function useLazyAsyncData<
key: string,
handler: (ctx?: NuxtApp) => Promise<ResT>,
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
export function useLazyAsyncData<
ResT,
DataE = Error,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null> {
DefaultT = DefaultAsyncDataValue,
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue> {
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
const [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]
@ -463,12 +467,12 @@ export function useLazyAsyncData<
}
/** @since 3.1.0 */
export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | null> } {
export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | DefaultAsyncDataValue> } {
const nuxtApp = useNuxtApp()
// Initialize value when key is not already set
if (!(key in nuxtApp.payload.data)) {
nuxtApp.payload.data[key] = null
nuxtApp.payload.data[key] = asyncDataDefaults.value
}
return {
@ -520,12 +524,12 @@ function clearNuxtDataByKey (nuxtApp: NuxtApp, key: string): void {
}
if (key in nuxtApp.payload._errors) {
nuxtApp.payload._errors[key] = null
nuxtApp.payload._errors[key] = asyncDataDefaults.errorValue
}
if (nuxtApp._asyncData[key]) {
nuxtApp._asyncData[key]!.data.value = undefined
nuxtApp._asyncData[key]!.error.value = null
nuxtApp._asyncData[key]!.error.value = asyncDataDefaults.errorValue
nuxtApp._asyncData[key]!.pending.value = false
nuxtApp._asyncData[key]!.status.value = 'idle'
}

View File

@ -4,6 +4,9 @@ import { toRef } from 'vue'
import { useNuxtApp } from '../nuxt'
import { useRouter } from './router'
// @ts-expect-error virtual file
import { nuxtDefaultErrorValue } from '#build/nuxt.config.mjs'
export const NUXT_ERROR_SIGNATURE = '__nuxt_error'
/** @since 3.0.0 */
@ -47,7 +50,7 @@ export const clearError = async (options: { redirect?: string } = {}) => {
await useRouter().replace(options.redirect)
}
error.value = null
error.value = nuxtDefaultErrorValue
}
/** @since 3.0.0 */

View File

@ -8,6 +8,9 @@ import { useRequestFetch } from './ssr'
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom } from './asyncData'
import { useAsyncData } from './asyncData'
// TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
// @ts-expect-error virtual file
import { fetchDefaults } from '#build/nuxt.config.mjs'
@ -30,7 +33,7 @@ export interface UseFetchOptions<
ResT,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
R extends NitroFetchRequest = string & {},
M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>,
> extends Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'watch'>, ComputedFetchOptions<R, M> {
@ -54,11 +57,11 @@ export function useFetch<
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
/**
* Fetch data from an API endpoint with an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-fetch}
@ -77,7 +80,7 @@ export function useFetch<
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
export function useFetch<
ResT = void,
ErrorT = FetchError,
@ -86,7 +89,7 @@ export function useFetch<
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>,
@ -190,11 +193,11 @@ export function useLazyFetch<
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
export function useLazyFetch<
ResT = void,
ErrorT = FetchError,
@ -207,7 +210,7 @@ export function useLazyFetch<
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
export function useLazyFetch<
ResT = void,
ErrorT = FetchError,
@ -216,7 +219,7 @@ export function useLazyFetch<
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
DefaultT = DefaultAsyncDataValue,
> (
request: Ref<ReqT> | ReqT | (() => ReqT),
arg1?: string | Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>,

View File

@ -0,0 +1,2 @@
// TODO: temporary module for backwards compatibility
export {}

View File

@ -23,6 +23,8 @@ import type { ViewTransition } from './plugins/view-transitions.client'
// @ts-expect-error virtual file
import { appId } from '#build/nuxt.config.mjs'
// TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultErrorValue } from '#app/defaults'
import type { NuxtAppLiterals } from '#app'
function getNuxtAppCtx (appName = appId || 'nuxt-app') {
@ -92,8 +94,8 @@ export interface NuxtPayload {
state: Record<string, any>
once: Set<string>
config?: Pick<RuntimeConfig, 'public' | 'app'>
error?: NuxtError | null
_errors: Record<string, NuxtError | null>
error?: NuxtError | DefaultErrorValue
_errors: Record<string, NuxtError | DefaultAsyncDataErrorValue>
[key: string]: unknown
}
@ -122,7 +124,7 @@ interface _NuxtApp {
_asyncData: Record<string, {
data: Ref<any>
pending: Ref<boolean>
error: Ref<Error | null>
error: Ref<Error | DefaultAsyncDataErrorValue>
status: Ref<AsyncDataRequestStatus>
} | undefined>

View File

@ -130,6 +130,12 @@ declare module '#app' {
}
}
declare module '#app/defaults' {
type DefaultAsyncDataErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
type DefaultAsyncDataValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
type DefaultErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
}
declare module 'vue' {
interface ComponentCustomProperties extends NuxtAppInjections { }
}
@ -393,7 +399,12 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`,
`export const devLogs = ${JSON.stringify(ctx.nuxt.options.features.devLogs)}`,
`export const nuxtLinkDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.nuxtLink)}`,
`export const asyncDataDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.useAsyncData)}`,
`export const asyncDataDefaults = ${JSON.stringify({
...ctx.nuxt.options.experimental.defaults.useAsyncData,
value: ctx.nuxt.options.experimental.defaults.useAsyncData.value === 'null' ? null : undefined,
errorValue: ctx.nuxt.options.experimental.defaults.useAsyncData.errorValue === 'null' ? null : undefined,
})}`,
`export const nuxtDefaultErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}`,
`export const fetchDefaults = ${JSON.stringify(fetchDefaults)}`,
`export const vueAppRootContainer = ${ctx.nuxt.options.app.rootId ? `'#${ctx.nuxt.options.app.rootId}'` : `'body > ${ctx.nuxt.options.app.rootTag}'`}`,
`export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`,

View File

@ -415,6 +415,18 @@ export default defineUntypedSchema({
* Options that apply to `useAsyncData` (and also therefore `useFetch`)
*/
useAsyncData: {
/** @type {'undefined' | 'null'} */
value: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'undefined' : 'null')
},
},
/** @type {'undefined' | 'null'} */
errorValue: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'undefined' : 'null')
},
},
deep: {
async $resolve (val, get) {
return val ?? !((await get('future') as Record<string, unknown>).compatibilityVersion === 4)

View File

@ -2433,21 +2433,23 @@ describe.skipIf(isWindows)('useAsyncData', () => {
})
it('data is null after navigation when immediate false', async () => {
const defaultValue = isV4 ? 'undefined' : 'null'
const { page } = await renderPage('/useAsyncData/immediate-remove-unmounted')
expect(await page.locator('#immediate-data').getByText('null').textContent()).toBe('null')
expect(await page.locator('#immediate-data').getByText(defaultValue).textContent()).toBe(defaultValue)
await page.click('#execute-btn')
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain('null')
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain(defaultValue)
await page.click('#to-index')
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/')
await page.click('#to-immediate-remove-unmounted')
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/useAsyncData/immediate-remove-unmounted')
expect(await page.locator('#immediate-data').getByText('null').textContent()).toBe('null')
expect(await page.locator('#immediate-data').getByText(defaultValue).textContent()).toBe(defaultValue)
await page.click('#execute-btn')
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain('null')
expect(await page.locator('#immediate-data').getByText(',').textContent()).not.toContain(defaultValue)
await page.close()
})

View File

@ -11,6 +11,9 @@ import type { NavigateToOptions } from '#app/composables/router'
import { NuxtLayout, NuxtLink, NuxtPage, ServerComponent, WithTypes } from '#components'
import { useRouter } from '#imports'
// TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
interface TestResponse { message: string }
describe('API routes', () => {
@ -31,61 +34,61 @@ describe('API routes', () => {
})
it('works with useAsyncData', () => {
expectTypeOf(useAsyncData('api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useAsyncData('api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useAsyncData('api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useAsyncData('api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useAsyncData('api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useAsyncData('api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-other', () => $fetch('/api/other')).data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useAsyncData<TestResponse>('api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useAsyncData<TestResponse>('api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<unknown> | null>>()
expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | null>>()
expectTypeOf(useAsyncData('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<unknown> | DefaultAsyncDataErrorValue>>()
expectTypeOf(useAsyncData<any, string>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | DefaultAsyncDataErrorValue>>()
// backwards compatibility
expectTypeOf(useAsyncData<any, Error>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | null>>()
expectTypeOf(useAsyncData<any, NuxtError<string>>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | null>>()
expectTypeOf(useAsyncData<any, Error>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | DefaultAsyncDataErrorValue>>()
expectTypeOf(useAsyncData<any, NuxtError<string>>('api-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<NuxtError<string> | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useLazyAsyncData('lazy-api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-hey-with-pick', () => $fetch('/api/hey'), { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-union', () => $fetch('/api/union')).data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-union-with-pick', () => $fetch('/api/union'), { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-api-other', () => $fetch('/api/other')).data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useLazyAsyncData<TestResponse>('lazy-api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useLazyAsyncData<TestResponse>('lazy-api-generics', () => $fetch('/test')).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | null>>()
expectTypeOf(useLazyAsyncData<any, string>('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyAsyncData('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<Error | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyAsyncData<any, string>('lazy-error-generics', () => $fetch('/error')).error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
})
it('works with useFetch', () => {
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf<Ref<{ method: 'post' } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | DefaultAsyncDataValue>>()
// @ts-expect-error not a valid method
useFetch('/api/hey', { method: 'PATCH' })
expectTypeOf(useFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useFetch<TestResponse>('/test', { method: 'POST' }).data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useFetch<TestResponse>('/test', { method: 'POST' }).data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useFetch('/error').error).toEqualTypeOf<Ref<FetchError | null>>()
expectTypeOf(useFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch('/error').error).toEqualTypeOf<Ref<FetchError | DefaultAsyncDataErrorValue>>()
expectTypeOf(useFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useLazyFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | null>>()
expectTypeOf(useLazyFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | null>>()
expectTypeOf(useLazyFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | null>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/hey', { pick: ['baz'] }).data).toEqualTypeOf<Ref<{ baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/union').data).toEqualTypeOf<Ref<{ type: 'a', foo: string } | { type: 'b', baz: string } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/union', { pick: ['type'] }).data).toEqualTypeOf<Ref<{ type: 'a' } | { type: 'b' } | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/other').data).toEqualTypeOf<Ref<unknown>>()
expectTypeOf(useLazyFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | null>>()
expectTypeOf(useLazyFetch<TestResponse>('/test').data).toEqualTypeOf<Ref<TestResponse | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/error').error).toEqualTypeOf<Ref<FetchError | null>>()
expectTypeOf(useLazyFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch('/error').error).toEqualTypeOf<Ref<FetchError | DefaultAsyncDataErrorValue>>()
expectTypeOf(useLazyFetch<any, string>('/error').error).toEqualTypeOf<Ref<string | DefaultAsyncDataErrorValue>>()
})
})
@ -421,10 +424,10 @@ describe('composables', () => {
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { default: () => 'test' }).data).toEqualTypeOf<Ref<string>>()
// transform must match the explicit generic because of typescript limitations microsoft/TypeScript#14400
expectTypeOf(useFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch<string>('/test', { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyAsyncData<string>(() => $fetch('/test'), { transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useFetch<string>('/test', { default: () => 'test', transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string>>()
expectTypeOf(useLazyFetch<string>('/test', { default: () => 'test', transform: () => 'transformed' }).data).toEqualTypeOf<Ref<string>>()
@ -439,7 +442,7 @@ describe('composables', () => {
return data.foo
},
})
expectTypeOf(data).toEqualTypeOf<Ref<'bar' | null>>()
expectTypeOf(data).toEqualTypeOf<Ref<'bar' | DefaultAsyncDataValue>>()
})
it('infer request url string literal from server/api routes', () => {
@ -448,8 +451,8 @@ describe('composables', () => {
expectTypeOf(useFetch(dynamicStringUrl).data).toEqualTypeOf<Ref<unknown>>()
// request param should infer string literal type / show auto-complete hint base on server routes, ex: '/api/hello'
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useLazyFetch('/api/hello').data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
// request can accept string literal and Request object type
expectTypeOf(useFetch('https://example.com/api').data).toEqualTypeOf<Ref<unknown>>()
@ -519,7 +522,7 @@ describe('composables', () => {
it('correctly types returns when using with getCachedData', () => {
expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: 1 }), {
getCachedData: key => useNuxtApp().payload.data[key],
}).data).toEqualTypeOf<Ref<{ foo: number } | null>>()
}).data).toEqualTypeOf<Ref<{ foo: number } | DefaultAsyncDataValue>>()
useAsyncData('test', () => Promise.resolve({ foo: 1 }), {
// @ts-expect-error cached data should return the same as value of fetcher
getCachedData: () => ({ bar: 2 }),

View File

@ -2,7 +2,7 @@
<div>
<div>immediate-remove-unmounted.vue</div>
<div id="immediate-data">
{{ data === null ? "null" : data }}
{{ data === null ? "null" : (data === undefined ? 'undefined' : data) }}
</div>
<button
id="execute-btn"
@ -20,9 +20,11 @@
</template>
<script setup lang="ts">
import { asyncDataDefaults } from '#build/nuxt.config.mjs'
const { data, execute } = await useAsyncData('immediateFalse', () => $fetch('/api/random'), { immediate: false })
if (data.value !== null) {
throw new Error('Initial data should be null: ' + data.value)
if (data.value !== asyncDataDefaults.errorValue) {
throw new Error(`Initial data should be ${asyncDataDefaults.errorValue}: ` + data.value)
}
</script>

View File

@ -20,6 +20,9 @@ import { callOnce } from '#app/composables/once'
import { useLoadingIndicator } from '#app/composables/loading-indicator'
import { useRouteAnnouncer } from '#app/composables/route-announcer'
// @ts-expect-error virtual file
import { asyncDataDefaults, nuxtDefaultErrorValue } from '#build/nuxt.config.mjs'
registerEndpoint('/api/test', defineEventHandler(event => ({
method: event.method,
headers: Object.fromEntries(event.headers.entries()),
@ -126,7 +129,7 @@ describe('useAsyncData', () => {
]
`)
expect(res instanceof Promise).toBeTruthy()
expect(res.data.value).toBe(null)
expect(res.data.value).toBe(asyncDataDefaults.value)
await res
expect(res.data.value).toBe('test')
})
@ -138,7 +141,7 @@ describe('useAsyncData', () => {
expect(immediate.pending.value).toBe(false)
const nonimmediate = await useAsyncData(() => Promise.resolve('test'), { immediate: false })
expect(nonimmediate.data.value).toBe(null)
expect(nonimmediate.data.value).toBe(asyncDataDefaults.value)
expect(nonimmediate.status.value).toBe('idle')
expect(nonimmediate.pending.value).toBe(true)
})
@ -163,9 +166,9 @@ describe('useAsyncData', () => {
// https://github.com/nuxt/nuxt/issues/23411
it('should initialize with error set to null when immediate: false', async () => {
const { error, execute } = useAsyncData(() => ({}), { immediate: false })
expect(error.value).toBe(null)
expect(error.value).toBe(asyncDataDefaults.errorValue)
await execute()
expect(error.value).toBe(null)
expect(error.value).toBe(asyncDataDefaults.errorValue)
})
it('should be accessible with useNuxtData', async () => {
@ -206,8 +209,9 @@ describe('useAsyncData', () => {
clear()
// TODO: update to asyncDataDefaults.value in v4
expect(data.value).toBeUndefined()
expect(error.value).toBeNull()
expect(error.value).toBe(asyncDataDefaults.errorValue)
expect(pending.value).toBe(false)
expect(status.value).toBe('idle')
})
@ -345,13 +349,12 @@ describe('errors', () => {
})
it('global nuxt errors', () => {
const err = useError()
expect(err.value).toBeUndefined()
const error = useError()
expect(error.value).toBeUndefined()
showError('new error')
expect(err.value).toMatchInlineSnapshot('[Error: new error]')
expect(error.value).toMatchInlineSnapshot('[Error: new error]')
clearError()
// TODO: should this return to being undefined?
expect(err.value).toBeNull()
expect(error.value).toBe(nuxtDefaultErrorValue)
})
})
@ -616,7 +619,7 @@ describe('routing utilities: `abortNavigation`', () => {
it('should throw an error if one is provided', () => {
const error = useError()
expect(() => abortNavigation({ message: 'Page not found' })).toThrowErrorMatchingInlineSnapshot('[Error: Page not found]')
expect(error.value).toBeFalsy()
expect(error.value).toBe(nuxtDefaultErrorValue)
})
it('should block navigation if no error is provided', () => {
expect(abortNavigation()).toMatchInlineSnapshot('false')