Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 21 commits into from Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/3.api/2.composables/use-async-data.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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[]
Expand All @@ -122,7 +126,7 @@ type AsyncData<DataT, ErrorT> = {
};

interface AsyncDataExecuteOptions {
dedupe?: boolean
dedupe?: 'cancel' | 'defer'
}

type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
Expand Down
6 changes: 5 additions & 1 deletion docs/3.api/2.composables/use-fetch.md
Expand Up @@ -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`.
Expand Down Expand Up @@ -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[]
Expand All @@ -152,7 +156,7 @@ type AsyncData<DataT, ErrorT> = {
}

interface AsyncDataExecuteOptions {
dedupe?: boolean
dedupe?: 'cancel' | 'defer'
}

type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
Expand Down
13 changes: 11 additions & 2 deletions packages/nuxt/src/app/composables/asyncData.ts
Expand Up @@ -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> {
Expand All @@ -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,
Expand Down Expand Up @@ -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)

Expand All @@ -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]!
}
Expand Down
27 changes: 27 additions & 0 deletions test/nuxt/composables.test.ts
Expand Up @@ -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', () => {
Expand Down