feat(nuxt): add dedupe option for data fetching composables (#24564)

This commit is contained in:
Eugen Guriev 2023-12-14 13:08:43 +02:00 committed by GitHub
parent 17b5ed9ad8
commit 8ccafb182d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 48 additions and 4 deletions

View File

@ -64,6 +64,9 @@ const { data: posts } = await useAsyncData(
- `pick`: only pick specified keys in this array from the `handler` function result
- `watch`: watch reactive sources to auto-refresh
- `deep`: 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.
- `dedupe`: avoid fetching same key more than once at a time (defaults to `cancel`). Possible options:
- `cancel` - cancels existing requests when a new one is made
- `defer` - does not make new requests at all if there is a pending request
::callout
Under the hood, `lazy: false` uses `<Suspense>` to block the loading of the route before the data has been fetched. Consider using `lazy: true` and implementing a loading state instead for a snappier user experience.
@ -105,6 +108,7 @@ type AsyncDataOptions<DataT> = {
lazy?: boolean
immediate?: boolean
deep?: boolean
dedupe?: 'cancel' | 'defer'
default?: () => DataT | Ref<DataT> | null
transform?: (input: DataT) => DataT
pick?: string[]
@ -122,7 +126,7 @@ type AsyncData<DataT, ErrorT> = {
};
interface AsyncDataExecuteOptions {
dedupe?: boolean
dedupe?: 'cancel' | 'defer'
}
type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'

View File

@ -96,6 +96,9 @@ All fetch options can be given a `computed` or `ref` value. These will be watche
- `pick`: only pick specified keys in this array from the `handler` function result
- `watch`: 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`. Together with `immediate: false`, this allows for a fully-manual `useFetch`.
- `deep`: 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.
- `dedupe`: avoid fetching same key more than once at a time (defaults to `cancel`). Possible options:
- `cancel` - cancels existing requests when a new one is made
- `defer` - does not make new requests at all if there is a pending request
::callout
If you provide a function or ref as the `url` parameter, or if you provide functions as arguments to the `options` parameter, then the `useFetch` call will not match other `useFetch` calls elsewhere in your codebase, even if the options seem to be identical. If you wish to force a match, you may provide your own key in `options`.
@ -136,6 +139,7 @@ type UseFetchOptions<DataT> = {
immediate?: boolean
getCachedData?: (key: string) => DataT
deep?: boolean
dedupe?: 'cancel' | 'defer'
default?: () => DataT
transform?: (input: DataT) => DataT
pick?: string[]
@ -152,7 +156,7 @@ type AsyncData<DataT, ErrorT> = {
}
interface AsyncDataExecuteOptions {
dedupe?: boolean
dedupe?: 'cancel' | 'defer'
}
type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'

View File

@ -49,16 +49,21 @@ export interface AsyncDataOptions<
watch?: MultiWatchSources
immediate?: boolean
deep?: boolean
dedupe?: 'cancel' | 'defer'
}
export interface AsyncDataExecuteOptions {
_initial?: boolean
// TODO: deprecate boolean option in future minor
/**
* Force a refresh, even if there is already a pending request. Previous requests will
* not be cancelled, but their result will not affect the data/pending state - and any
* previously awaited promises will not resolve until this new request resolves.
*
* Instead of using `boolean` values, use `cancel` for `true` and `defer` for `false`.
* Boolean values will be removed in a future release.
*/
dedupe?: boolean
dedupe?: boolean | 'cancel' | 'defer'
}
export interface _AsyncData<DataT, ErrorT> {
@ -72,6 +77,9 @@ export interface _AsyncData<DataT, ErrorT> {
export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>>
// TODO: deprecate boolean option in future minor
const isDefer = (dedupe?: boolean | 'cancel' | 'defer') => dedupe === 'defer' || dedupe === false
export function useAsyncData<
ResT,
DataE = Error,
@ -150,6 +158,7 @@ export function useAsyncData<
options.lazy = options.lazy ?? false
options.immediate = options.immediate ?? true
options.deep = options.deep ?? asyncDataDefaults.deep
options.dedupe = options.dedupe ?? 'cancel'
const hasCachedData = () => ![null, undefined].includes(options.getCachedData!(key) as any)
@ -172,7 +181,7 @@ export function useAsyncData<
asyncData.refresh = asyncData.execute = (opts = {}) => {
if (nuxt._asyncDataPromises[key]) {
if (opts.dedupe === false) {
if (isDefer(opts.dedupe ?? options.dedupe)) {
// Avoid fetching same key more than once at a time
return nuxt._asyncDataPromises[key]!
}

View File

@ -239,6 +239,33 @@ describe('useAsyncData', () => {
const { data } = await useAsyncData(() => Promise.reject(new Error('test')), { default: () => 'default' })
expect(data.value).toMatchInlineSnapshot('"default"')
})
it('should execute the promise function once when dedupe option is "defer" for multiple calls', async () => {
const promiseFn = vi.fn(() => Promise.resolve('test'))
useAsyncData('dedupedKey', promiseFn, { dedupe: 'defer' })
useAsyncData('dedupedKey', promiseFn, { dedupe: 'defer' })
useAsyncData('dedupedKey', promiseFn, { dedupe: 'defer' })
expect(promiseFn).toHaveBeenCalledTimes(1)
})
it('should execute the promise function multiple times when dedupe option is not specified for multiple calls', async () => {
const promiseFn = vi.fn(() => Promise.resolve('test'))
useAsyncData('dedupedKey', promiseFn)
useAsyncData('dedupedKey', promiseFn)
useAsyncData('dedupedKey', promiseFn)
expect(promiseFn).toHaveBeenCalledTimes(3)
})
it('should execute the promise function as per dedupe option when different dedupe options are used for multiple calls', async () => {
const promiseFn = vi.fn(() => Promise.resolve('test'))
useAsyncData('dedupedKey', promiseFn, { dedupe: 'defer' })
useAsyncData('dedupedKey', promiseFn)
useAsyncData('dedupedKey', promiseFn, { dedupe: 'defer' })
expect(promiseFn).toHaveBeenCalledTimes(2)
})
})
describe('useFetch', () => {