From 951ffd6e01427da5b5419e790b0e6eb533747044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Thu, 18 Jan 2024 17:02:52 +0100 Subject: [PATCH] feat(nuxt): warn if data fetch composables are used wrongly (#25071) --- .../nuxt/src/app/composables/asyncData.ts | 74 ++++++++++++++++++- packages/nuxt/src/app/composables/fetch.ts | 24 +++++- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index bbdcaabb93..ee40a814c7 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -42,15 +42,51 @@ export interface AsyncDataOptions< PickKeys extends KeysOf = KeysOf, DefaultT = null, > { + /** + * Whether to fetch on the server side. + * @default true + */ server?: boolean + /** + * Whether to resolve the async function after loading the route, instead of blocking client-side navigation + * @default false + */ lazy?: boolean + /** + * a factory function to set the default value of the data, before the async function resolves - useful with the `lazy: true` or `immediate: false` options + */ default?: () => DefaultT | Ref + /** + * Provide a function which returns cached data. + * A `null` or `undefined` return value will trigger a fetch. + * Default is `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]` which only caches data when payloadExtraction is enabled. + */ getCachedData?: (key: string) => DataT + /** + * A function that can be used to alter handler function result after resolving + */ transform?: _Transform + /** + * Only pick specified keys in this array from the handler function result + */ pick?: PickKeys + /** + * Watch reactive sources to auto-refresh when changed + */ watch?: MultiWatchSources + /** + * When set to false, will prevent the request from firing immediately + * @default true + */ immediate?: boolean + /** + * Return data in a deep ref object (it is true by default). It can be set to false to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive. + */ deep?: boolean + /** + * Avoid fetching the same key more than once at a time + * @default 'cancel' + */ dedupe?: 'cancel' | 'defer' } @@ -82,6 +118,12 @@ export type AsyncData = _AsyncData & Promise<_AsyncDat // TODO: deprecate boolean option in future minor const isDefer = (dedupe?: boolean | 'cancel' | 'defer') => dedupe === 'defer' || dedupe === false +/** + * Provides access to data that resolves asynchronously in an SSR-friendly composable. + * See {@link https://nuxt.com/docs/api/composables/use-async-data} + * @param handler An asynchronous function that must return a truthy value (for example, it should not be `undefined` or `null`) or the request may be duplicated on the client side. + * @param options customize the behavior of useAsyncData + */ export function useAsyncData< ResT, NuxtErrorDataT = unknown, @@ -92,6 +134,12 @@ export function useAsyncData< handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions ): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | null> +/** + * Provides access to data that resolves asynchronously in an SSR-friendly composable. + * See {@link https://nuxt.com/docs/api/composables/use-async-data} + * @param handler An asynchronous function that must return a truthy value (for example, it should not be `undefined` or `null`) or the request may be duplicated on the client side. + * @param options customize the behavior of useAsyncData + */ export function useAsyncData< ResT, NuxtErrorDataT = unknown, @@ -102,6 +150,13 @@ export function useAsyncData< handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions ): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | null> +/** + * Provides access to data that resolves asynchronously in an SSR-friendly composable. + * See {@link https://nuxt.com/docs/api/composables/use-async-data} + * @param key A unique key to ensure that data fetching can be properly de-duplicated across requests. + * @param handler An asynchronous function that must return a truthy value (for example, it should not be `undefined` or `null`) or the request may be duplicated on the client side. + * @param options customize the behavior of useAsyncData + */ export function useAsyncData< ResT, NuxtErrorDataT = unknown, @@ -113,6 +168,13 @@ export function useAsyncData< handler: (ctx?: NuxtApp) => Promise, options?: AsyncDataOptions ): AsyncData | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError) | null> +/** + * Provides access to data that resolves asynchronously in an SSR-friendly composable. + * See {@link https://nuxt.com/docs/api/composables/use-async-data} + * @param key A unique key to ensure that data fetching can be properly de-duplicated across requests. + * @param handler An asynchronous function that must return a truthy value (for example, it should not be `undefined` or `null`) or the request may be duplicated on the client side. + * @param options customize the behavior of useAsyncData + */ export function useAsyncData< ResT, NuxtErrorDataT = unknown, @@ -269,6 +331,10 @@ export function useAsyncData< if (import.meta.client) { // Setup hook callbacks once per instance const instance = getCurrentInstance() + if (import.meta.dev && !nuxt.isHydrating && (!instance || instance?.isMounted)) { + // @ts-expect-error private property + console.warn(`[nuxt] [${options._functionName || 'useAsyncData'}] Component is already mounted, please use $fetch instead. See https://nuxt.com/docs/getting-started/data-fetching`) + } if (instance && !instance._nuxtOnBeforeMountCbs) { instance._nuxtOnBeforeMountCbs = [] const cbs = instance._nuxtOnBeforeMountCbs @@ -364,7 +430,13 @@ export function useLazyAsyncData< > (...args: any[]): AsyncData | DefaultT, DataE | null> { 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, AsyncDataOptions] + const [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] + + if (import.meta.dev && import.meta.client) { + // @ts-expect-error private property + options._functionName ||= 'useLazyAsyncData' + } + // @ts-expect-error we pass an extra argument to prevent a key being injected return useAsyncData(key, handler, { ...options, lazy: true }, null) } diff --git a/packages/nuxt/src/app/composables/fetch.ts b/packages/nuxt/src/app/composables/fetch.ts index 90d830c0a5..2961f19732 100644 --- a/packages/nuxt/src/app/composables/fetch.ts +++ b/packages/nuxt/src/app/composables/fetch.ts @@ -39,6 +39,12 @@ export interface UseFetchOptions< watch?: MultiWatchSources | false } +/** + * Fetch data from an API endpoint with an SSR-friendly composable. + * See {@link https://nuxt.com/docs/api/composables/use-fetch} + * @param request The URL to fetch + * @param opts extends $fetch options and useAsyncData options + */ export function useFetch< ResT = void, ErrorT = FetchError, @@ -52,6 +58,12 @@ export function useFetch< request: Ref | ReqT | (() => ReqT), opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method> ): AsyncData | DefaultT, ErrorT | null> +/** + * Fetch data from an API endpoint with an SSR-friendly composable. + * See {@link https://nuxt.com/docs/api/composables/use-fetch} + * @param request The URL to fetch + * @param opts extends $fetch options and useAsyncData options + */ export function useFetch< ResT = void, ErrorT = FetchError, @@ -134,6 +146,11 @@ export function useFetch< watch: watch === false ? [] : [_fetchOptions, _request, ...(watch || [])] } + if (import.meta.dev && import.meta.client) { + // @ts-expect-error private property + _asyncDataOptions._functionName = opts._functionName || 'useFetch' + } + let controller: AbortController const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys, DefaultT>(key, () => { @@ -207,7 +224,12 @@ export function useLazyFetch< arg1?: string | Omit, 'lazy'>, arg2?: string ) { - const [opts, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] + const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] + + if (import.meta.dev && import.meta.client) { + // @ts-expect-error private property + opts._functionName ||= 'useLazyFetch' + } return useFetch(request, { ...opts,