mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-13 09:33:54 +00:00
feat(nuxt): custom cache support for data fetching composables (#20747)
This commit is contained in:
parent
f4d67a9bcd
commit
b52548d915
@ -31,6 +31,7 @@ type AsyncDataOptions<DataT> = {
|
|||||||
transform?: (input: DataT) => DataT
|
transform?: (input: DataT) => DataT
|
||||||
pick?: string[]
|
pick?: string[]
|
||||||
watch?: WatchSource[]
|
watch?: WatchSource[]
|
||||||
|
getCachedData?: (key: string) => any
|
||||||
}
|
}
|
||||||
|
|
||||||
type AsyncData<DataT, ErrorT> = {
|
type AsyncData<DataT, ErrorT> = {
|
||||||
@ -61,6 +62,7 @@ type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
|
|||||||
* _transform_: a function that can be used to alter `handler` function result after resolving
|
* _transform_: a function that can be used to alter `handler` function result after resolving
|
||||||
* _pick_: only pick specified keys in this array from the `handler` function result
|
* _pick_: only pick specified keys in this array from the `handler` function result
|
||||||
* _watch_: watch reactive sources to auto-refresh
|
* _watch_: watch reactive sources to auto-refresh
|
||||||
|
* _getCachedData_: a function that receives a cache key and can return cached data if it exists (by default it returns `nuxtApp.payload.data[key]` when hydrating and `nuxtApp.static.data[key]` after the app is hydrated). You can use this to build your own custom cache for `useAsyncData`.
|
||||||
* _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.
|
* _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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
@ -26,6 +26,7 @@ type UseFetchOptions<DataT> = {
|
|||||||
server?: boolean
|
server?: boolean
|
||||||
lazy?: boolean
|
lazy?: boolean
|
||||||
immediate?: boolean
|
immediate?: boolean
|
||||||
|
getCachedData?: (key: string) => any
|
||||||
deep?: boolean
|
deep?: boolean
|
||||||
default?: () => DataT
|
default?: () => DataT
|
||||||
transform?: (input: DataT) => DataT
|
transform?: (input: DataT) => DataT
|
||||||
@ -73,6 +74,7 @@ All fetch options can be given a `computed` or `ref` value. These will be watche
|
|||||||
* `transform`: a function that can be used to alter `handler` function result after resolving
|
* `transform`: a function that can be used to alter `handler` function result after resolving
|
||||||
* `pick`: only pick specified keys in this array from the `handler` function result
|
* `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`.
|
* `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`.
|
||||||
|
* `getCachedData`: a function that receives a cache key and can return cached data if it exists (by default it returns `nuxtApp.payload.data[key]` when hydrating and `nuxtApp.static.data[key]` after the app is hydrated). You can use this to build your own custom cache for `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.
|
* `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.
|
||||||
|
|
||||||
::alert{type=warning}
|
::alert{type=warning}
|
||||||
|
@ -40,6 +40,7 @@ export interface AsyncDataOptions<
|
|||||||
server?: boolean
|
server?: boolean
|
||||||
lazy?: boolean
|
lazy?: boolean
|
||||||
default?: () => DefaultT | Ref<DefaultT>
|
default?: () => DefaultT | Ref<DefaultT>
|
||||||
|
getCachedData?: (key: string) => DataT
|
||||||
transform?: _Transform<ResT, DataT>
|
transform?: _Transform<ResT, DataT>
|
||||||
pick?: PickKeys
|
pick?: PickKeys
|
||||||
watch?: MultiWatchSources
|
watch?: MultiWatchSources
|
||||||
@ -60,15 +61,14 @@ export interface AsyncDataExecuteOptions {
|
|||||||
export interface _AsyncData<DataT, ErrorT> {
|
export interface _AsyncData<DataT, ErrorT> {
|
||||||
data: Ref<DataT>
|
data: Ref<DataT>
|
||||||
pending: Ref<boolean>
|
pending: Ref<boolean>
|
||||||
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
|
refresh: (opts?: AsyncDataExecuteOptions) => Promise<DataT>
|
||||||
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
|
execute: (opts?: AsyncDataExecuteOptions) => Promise<DataT>
|
||||||
error: Ref<ErrorT | null>
|
error: Ref<ErrorT | null>
|
||||||
status: Ref<AsyncDataRequestStatus>
|
status: Ref<AsyncDataRequestStatus>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>>
|
export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>>
|
||||||
|
|
||||||
const getDefault = () => null
|
|
||||||
export function useAsyncData<
|
export function useAsyncData<
|
||||||
ResT,
|
ResT,
|
||||||
DataE = Error,
|
DataE = Error,
|
||||||
@ -132,20 +132,22 @@ export function useAsyncData<
|
|||||||
throw new TypeError('[nuxt] [asyncData] handler must be a function.')
|
throw new TypeError('[nuxt] [asyncData] handler must be a function.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup nuxt instance payload
|
||||||
|
const nuxt = useNuxtApp()
|
||||||
|
|
||||||
|
// Used to get default values
|
||||||
|
const getDefault = () => null
|
||||||
|
const getDefaultCachedData = () => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]
|
||||||
|
|
||||||
// Apply defaults
|
// Apply defaults
|
||||||
options.server = options.server ?? true
|
options.server = options.server ?? true
|
||||||
options.default = options.default ?? (getDefault as () => DefaultT)
|
options.default = options.default ?? (getDefault as () => DefaultT)
|
||||||
|
options.getCachedData = options.getCachedData ?? getDefaultCachedData
|
||||||
|
|
||||||
options.lazy = options.lazy ?? false
|
options.lazy = options.lazy ?? false
|
||||||
options.immediate = options.immediate ?? true
|
options.immediate = options.immediate ?? true
|
||||||
|
|
||||||
// Setup nuxt instance payload
|
const hasCachedData = () => ![null, undefined].includes(options.getCachedData!(key) as any)
|
||||||
const nuxt = useNuxtApp()
|
|
||||||
|
|
||||||
const getCachedData = () => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]
|
|
||||||
const hasCachedData = () => ![null, undefined].includes(
|
|
||||||
nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create or use a shared asyncData entity
|
// Create or use a shared asyncData entity
|
||||||
if (!nuxt._asyncData[key] || !options.immediate) {
|
if (!nuxt._asyncData[key] || !options.immediate) {
|
||||||
@ -154,7 +156,7 @@ export function useAsyncData<
|
|||||||
const _ref = options.deep !== true ? shallowRef : ref
|
const _ref = options.deep !== true ? shallowRef : ref
|
||||||
|
|
||||||
nuxt._asyncData[key] = {
|
nuxt._asyncData[key] = {
|
||||||
data: _ref(getCachedData() ?? options.default!()),
|
data: _ref(options.getCachedData!(key) ?? options.default!()),
|
||||||
pending: ref(!hasCachedData()),
|
pending: ref(!hasCachedData()),
|
||||||
error: toRef(nuxt.payload._errors, key),
|
error: toRef(nuxt.payload._errors, key),
|
||||||
status: ref('idle')
|
status: ref('idle')
|
||||||
@ -168,13 +170,13 @@ export function useAsyncData<
|
|||||||
if (nuxt._asyncDataPromises[key]) {
|
if (nuxt._asyncDataPromises[key]) {
|
||||||
if (opts.dedupe === false) {
|
if (opts.dedupe === false) {
|
||||||
// Avoid fetching same key more than once at a time
|
// Avoid fetching same key more than once at a time
|
||||||
return nuxt._asyncDataPromises[key]
|
return nuxt._asyncDataPromises[key]!
|
||||||
}
|
}
|
||||||
(nuxt._asyncDataPromises[key] as any).cancelled = true
|
(nuxt._asyncDataPromises[key] as any).cancelled = true
|
||||||
}
|
}
|
||||||
// Avoid fetching same key that is already fetched
|
// Avoid fetching same key that is already fetched
|
||||||
if ((opts._initial || (nuxt.isHydrating && opts._initial !== false)) && hasCachedData()) {
|
if ((opts._initial || (nuxt.isHydrating && opts._initial !== false)) && hasCachedData()) {
|
||||||
return getCachedData()
|
return Promise.resolve(options.getCachedData!(key))
|
||||||
}
|
}
|
||||||
asyncData.pending.value = true
|
asyncData.pending.value = true
|
||||||
asyncData.status.value = 'pending'
|
asyncData.status.value = 'pending'
|
||||||
@ -222,7 +224,7 @@ export function useAsyncData<
|
|||||||
delete nuxt._asyncDataPromises[key]
|
delete nuxt._asyncDataPromises[key]
|
||||||
})
|
})
|
||||||
nuxt._asyncDataPromises[key] = promise
|
nuxt._asyncDataPromises[key] = promise
|
||||||
return nuxt._asyncDataPromises[key]
|
return nuxt._asyncDataPromises[key]!
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialFetch = () => asyncData.refresh({ _initial: true })
|
const initialFetch = () => asyncData.refresh({ _initial: true })
|
||||||
@ -235,7 +237,7 @@ export function useAsyncData<
|
|||||||
if (getCurrentInstance()) {
|
if (getCurrentInstance()) {
|
||||||
onServerPrefetch(() => promise)
|
onServerPrefetch(() => promise)
|
||||||
} else {
|
} else {
|
||||||
nuxt.hook('app:created', () => promise)
|
nuxt.hook('app:created', async () => { await promise })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,9 +272,9 @@ export function useAsyncData<
|
|||||||
if (options.watch) {
|
if (options.watch) {
|
||||||
watch(options.watch, () => asyncData.refresh())
|
watch(options.watch, () => asyncData.refresh())
|
||||||
}
|
}
|
||||||
const off = nuxt.hook('app:data:refresh', (keys) => {
|
const off = nuxt.hook('app:data:refresh', async (keys) => {
|
||||||
if (!keys || keys.includes(key)) {
|
if (!keys || keys.includes(key)) {
|
||||||
return asyncData.refresh()
|
await asyncData.refresh()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (instance) {
|
if (instance) {
|
||||||
|
@ -107,6 +107,7 @@ export function useFetch<
|
|||||||
pick,
|
pick,
|
||||||
watch,
|
watch,
|
||||||
immediate,
|
immediate,
|
||||||
|
getCachedData,
|
||||||
deep,
|
deep,
|
||||||
...fetchOptions
|
...fetchOptions
|
||||||
} = opts
|
} = opts
|
||||||
@ -123,6 +124,7 @@ export function useFetch<
|
|||||||
transform,
|
transform,
|
||||||
pick,
|
pick,
|
||||||
immediate,
|
immediate,
|
||||||
|
getCachedData,
|
||||||
deep,
|
deep,
|
||||||
watch: watch === false ? [] : [_fetchOptions, _request, ...(watch || [])]
|
watch: watch === false ? [] : [_fetchOptions, _request, ...(watch || [])]
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,15 @@ describe('useAsyncData', () => {
|
|||||||
await refreshNuxtData('key')
|
await refreshNuxtData('key')
|
||||||
expect(data.data.value).toMatchInlineSnapshot('"test"')
|
expect(data.data.value).toMatchInlineSnapshot('"test"')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('allows custom access to a cache', async () => {
|
||||||
|
const { data } = await useAsyncData(() => ({ val: true }), { getCachedData: () => ({ val: false }) })
|
||||||
|
expect(data.value).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"val": false,
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('errors', () => {
|
describe('errors', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user