diff --git a/docs/api/my-api.md b/docs/api/my-api.md index d69a633..7f69264 100644 --- a/docs/api/my-api.md +++ b/docs/api/my-api.md @@ -6,6 +6,48 @@ Returns the raw response of the API endpoint. Intended for actions inside methods, e.g. when sending form data to the API when clicking a submit button. +## Type Declarations + +```ts +interface BaseApiFetchOptions { + /** + * Skip the Nuxt server proxy and fetch directly from the API. + * Requires `allowClient` to be enabled in the module options as well. + * @default false + */ + client?: boolean + /** + * Cache the response for the same request. + * If set to `true`, the cache key will be generated from the request options. + * Alternatively, a custom cache key can be provided. + * @default false + */ + cache?: string | boolean +} + +type ApiFetchOptions = Omit, 'body' | 'cache'> & { + pathParams?: Record + body?: string | Record | FormData | null +} + +function $Api( + path: string, + opts?: ApiFetchOptions & BaseApiFetchOptions +): Promise +``` + +## Caching + +By default, a [unique key is generated](/guide/caching) based in input parameters for each request to ensure that data fetching can be properly de-duplicated across requests. You can provide a custom key by passing a string as the `cache` option: + +```ts +const route = useRoute() + +const data = await $myApi('posts', { + cache: `posts-${route.params.id}` +}) +``` + ## Example ::: info @@ -90,31 +132,3 @@ export default defineNuxtConfig({ } }) ``` - -## Type Declarations - -```ts -interface BaseApiFetchOptions { - /** - * Skip the Nuxt server proxy and fetch directly from the API. - * Requires `allowClient` to be enabled in the module options as well. - * @default false - */ - client?: boolean - /** - * Cache the response for the same request - * @default false - */ - cache?: boolean -} - -type ApiFetchOptions = Omit, 'body' | 'cache'> & { - pathParams?: Record - body?: string | Record | FormData | null -} - -type $Api = ( - path: string, - opts?: ApiFetchOptions & BaseApiFetchOptions -) => Promise -``` diff --git a/docs/api/use-my-api-data.md b/docs/api/use-my-api-data.md index 6f40c2b..6a0d611 100644 --- a/docs/api/use-my-api-data.md +++ b/docs/api/use-my-api-data.md @@ -19,6 +19,71 @@ The composable supports every [`useAsyncData` option](https://nuxt.com/docs/api/ By default, Nuxt waits until a `refresh` is finished before it can be executed again. Passing `true` as parameter skips that wait. +## Type Declarations + +```ts +type BaseUseApiDataOptions = Omit, 'watch'> & { + /** + * Skip the Nuxt server proxy and fetch directly from the API. + * Requires `allowClient` to be enabled in the module options as well. + * @default false + */ + client?: boolean + /** + * Cache the response for the same request. + * @default true + */ + cache?: boolean + /** + * Watch an array of reactive sources and auto-refresh the fetch result when they change. + * Fetch options and URL are watched by default. You can completely ignore reactive sources by using `watch: false`. + */ + watch?: (WatchSource | object)[] | false +} + +type UseApiDataOptions = Pick< + ComputedOptions>, + | 'onRequest' + | 'onRequestError' + | 'onResponse' + | 'onResponseError' + | 'query' + | 'headers' + | 'method' + | 'retry' + | 'retryDelay' + | 'timeout' +> & { + pathParams?: MaybeRef> + body?: MaybeRef | FormData | null | undefined> +} & BaseUseApiDataOptions + +function UseApiData( + path: MaybeRefOrGetter, + opts?: UseApiDataOptions +): AsyncData +function UseApiData( + key: MaybeRefOrGetter, + path: MaybeRefOrGetter, + opts?: UseApiDataOptions +): AsyncData +``` + +## Caching + +By default, a [unique key is generated](/guide/caching) based in input parameters for each request to ensure that data fetching can be properly de-duplicated across requests. You can provide a custom key by passing a string as the first argument, just like the native [`useAsyncData`](https://nuxt.com/docs/api/composables/use-async-data): + +```ts +const route = useRoute() +const key = computed(() => `posts-${route.params.id}`) + +const { data } = await useMyApiData(key, 'posts') +``` + +::: tip +The key can be a reactive value, e.g. a computed property. +::: + ## Examples ::: info @@ -124,48 +189,3 @@ export default defineNuxtConfig({ } }) ``` - -## Type Declarations - -```ts -type BaseUseApiDataOptions = Omit, 'watch'> & { - /** - * Skip the Nuxt server proxy and fetch directly from the API. - * Requires `allowClient` to be enabled in the module options as well. - * @default false - */ - client?: boolean - /** - * Cache the response for the same request - * @default true - */ - cache?: boolean - /** - * Watch an array of reactive sources and auto-refresh the fetch result when they change. - * Fetch options and URL are watched by default. You can completely ignore reactive sources by using `watch: false`. - */ - watch?: (WatchSource | object)[] | false -} - -type UseApiDataOptions = Pick< - ComputedOptions>, - | 'onRequest' - | 'onRequestError' - | 'onResponse' - | 'onResponseError' - | 'query' - | 'headers' - | 'method' - | 'retry' - | 'retryDelay' - | 'timeout' -> & { - pathParams?: MaybeRef> - body?: MaybeRef | FormData | null | undefined> -} & BaseUseApiDataOptions - -type UseApiData = ( - path: MaybeRefOrGetter, - opts?: UseApiDataOptions -) => AsyncData -``` diff --git a/src/runtime/composables/$api.ts b/src/runtime/composables/$api.ts index 5289454..46bd960 100644 --- a/src/runtime/composables/$api.ts +++ b/src/runtime/composables/$api.ts @@ -15,10 +15,12 @@ export interface BaseApiFetchOptions { */ client?: boolean /** - * Cache the response for the same request + * Cache the response for the same request. + * If set to `true`, the cache key will be generated from the request options. + * Alternatively, a custom cache key can be provided. * @default false */ - cache?: boolean + cache?: string | boolean } export type ApiFetchOptions = Omit, 'body' | 'cache'> & { @@ -40,7 +42,7 @@ export interface $OpenApi> { path: P, opts: BaseApiFetchOptions & Omit, 'method'> ): Promise> -

, M extends IgnoreCase>( +

, M extends IgnoreCase>( path: P, opts?: BaseApiFetchOptions & OpenApiRequestOptions & { method: M } ): Promise]>> @@ -52,7 +54,9 @@ export function _$api( opts: ApiFetchOptions & BaseApiFetchOptions = {}, ): Promise { const nuxt = useNuxtApp() + const { apiParty } = useRuntimeConfig().public const promiseMap = (nuxt._promiseMap = nuxt._promiseMap || new Map()) as Map> + const { pathParams, query, @@ -63,24 +67,26 @@ export function _$api( cache = false, ...fetchOptions } = opts - const { apiParty } = useRuntimeConfig().public - const key = `$party${hash([ - endpointId, - path, - pathParams, - query, - method, - ...(isFormData(body) ? [] : [body]), - ])}` + + const _key = typeof cache === 'string' + ? cache + : `$party${hash([ + endpointId, + path, + pathParams, + query, + method, + ...(isFormData(body) ? [] : [body]), + ])}` if (client && !apiParty.allowClient) throw new Error('Client-side API requests are disabled. Set "allowClient: true" in the module options to enable them.') - if ((nuxt.isHydrating || cache) && key in nuxt.payload.data) - return Promise.resolve(nuxt.payload.data[key]) + if ((nuxt.isHydrating || cache) && _key in nuxt.payload.data) + return Promise.resolve(nuxt.payload.data[_key]) - if (promiseMap.has(key)) - return promiseMap.get(key)! + if (promiseMap.has(_key)) + return promiseMap.get(_key)! const endpoints = (apiParty as unknown as ModuleOptions).endpoints || {} const endpoint = endpoints[endpointId] @@ -120,19 +126,19 @@ export function _$api( const request = (client ? clientFetcher() : serverFetcher()) .then((response) => { if (process.server || cache) - nuxt.payload.data[key] = response - promiseMap.delete(key) + nuxt.payload.data[_key] = response + promiseMap.delete(_key) return response }) // Invalidate cache if request fails .catch((error) => { - if (key in nuxt.payload.data) - delete nuxt.payload.data[key] - promiseMap.delete(key) + if (_key in nuxt.payload.data) + delete nuxt.payload.data[_key] + promiseMap.delete(_key) throw error }) as Promise - promiseMap.set(key, request) + promiseMap.set(_key, request) return request } diff --git a/src/runtime/composables/useApiData.ts b/src/runtime/composables/useApiData.ts index 798a531..c5513d3 100644 --- a/src/runtime/composables/useApiData.ts +++ b/src/runtime/composables/useApiData.ts @@ -27,7 +27,7 @@ export type BaseUseApiDataOptions = Omit, 'watch'> & { */ client?: boolean /** - * Cache the response for the same request + * Cache the response for the same request. * @default true */ cache?: boolean @@ -60,10 +60,17 @@ export type UseOpenApiDataOptions< M extends IgnoreCase = IgnoreCase, > = BaseUseApiDataOptions]>> & ComputedOptions> -export type UseApiData = ( - path: MaybeRefOrGetter, - opts?: UseApiDataOptions, -) => AsyncData +export interface UseApiData { + ( + path: MaybeRefOrGetter, + opts?: UseApiDataOptions, + ): AsyncData + ( + key: MaybeRefOrGetter, + path: MaybeRefOrGetter, + opts?: UseApiDataOptions, + ): AsyncData +} export interface UseOpenApiData> {

>( @@ -78,13 +85,42 @@ export interface UseOpenApiData> { path: MaybeRefOrGetter

, opts: UseOpenApiDataOptions & { method: M }, ): AsyncData]> | undefined, FetchError]>>> + // Support for custom unique key +

>( + key: MaybeRefOrGetter, + path: MaybeRefOrGetter

, + opts?: Omit, 'method'>, + ): AsyncData | undefined, FetchError>> +

>( + key: MaybeRefOrGetter, + path: MaybeRefOrGetter

, + opts: Omit, 'method'>, + ): AsyncData | undefined, FetchError>> +

, M extends IgnoreCase>( + key: MaybeRefOrGetter, + path: MaybeRefOrGetter

, + opts: UseOpenApiDataOptions & { method: M }, + ): AsyncData]> | undefined, FetchError]>>> } +export function _useApiData( + endpointId: string, + key: MaybeRefOrGetter, + path: MaybeRefOrGetter, + opts: UseApiDataOptions, +): AsyncData export function _useApiData( endpointId: string, path: MaybeRefOrGetter, - opts: UseApiDataOptions = {}, + opts: UseApiDataOptions, +): AsyncData +export function _useApiData( + endpointId: string, + ...args: [MaybeRefOrGetter, UseApiDataOptions] | [MaybeRefOrGetter, MaybeRefOrGetter, UseApiDataOptions] ) { + const [key = undefined] = args.length === 3 ? [args[0]] : [] + const [path, opts = {}] = args.length === 3 ? [args[1], args[2]] : args + const { apiParty } = useRuntimeConfig().public const { server, @@ -103,7 +139,20 @@ export function _useApiData( cache = true, ...fetchOptions } = opts + const _path = computed(() => resolvePath(toValue(path), toValue(pathParams))) + const _key = computed(() => `$party${ + key + ? toValue(key) + : hash([ + endpointId, + _path.value, + toValue(query), + toValue(method), + ...(isFormData(toValue(body)) ? [] : [toValue(body)]), + ]) + }`, + ) if (client && !apiParty.allowClient) throw new Error('Client-side API requests are disabled. Set "allowClient: true" in the module options to enable them.') @@ -140,23 +189,16 @@ export function _useApiData( } let controller: AbortController | undefined - const key = computed(() => `$party${hash([ - endpointId, - _path.value, - toValue(query), - toValue(method), - ...(isFormData(toValue(body)) ? [] : [toValue(body)]), - ])}`) return useAsyncData( - key.value, + _key.value, async (nuxt) => { controller?.abort?.() // Workaround to persist response client-side // https://github.com/nuxt/nuxt/issues/15445 - if ((nuxt!.isHydrating || cache) && key.value in nuxt!.payload.data) - return nuxt!.payload.data[key.value] + if ((nuxt!.isHydrating || cache) && _key.value in nuxt!.payload.data) + return nuxt!.payload.data[_key.value] controller = new AbortController() @@ -197,14 +239,14 @@ export function _useApiData( } catch (error) { // Invalidate cache if request fails - if (key.value in nuxt!.payload.data) - delete nuxt!.payload.data[key.value] + if (_key.value in nuxt!.payload.data) + delete nuxt!.payload.data[_key.value] throw error } if (cache) - nuxt!.payload.data[key.value] = result + nuxt!.payload.data[_key.value] = result return result },