mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +00:00
feat: implement force + triggeredBy
This commit is contained in:
parent
59dd5fd939
commit
6136b3e219
@ -36,6 +36,7 @@ export type KeyOfRes<Transform extends _Transform> = KeysOf<ReturnType<Transform
|
|||||||
|
|
||||||
export type MultiWatchSources = (WatchSource<unknown> | object)[]
|
export type MultiWatchSources = (WatchSource<unknown> | object)[]
|
||||||
|
|
||||||
|
export type GetCachedDataTriggeredBy = 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch'
|
||||||
export interface AsyncDataOptions<
|
export interface AsyncDataOptions<
|
||||||
ResT,
|
ResT,
|
||||||
DataT = ResT,
|
DataT = ResT,
|
||||||
@ -60,8 +61,9 @@ export interface AsyncDataOptions<
|
|||||||
* Provide a function which returns cached data.
|
* Provide a function which returns cached data.
|
||||||
* A `null` or `undefined` return value will trigger a fetch.
|
* 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.
|
* Default is `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]` which only caches data when payloadExtraction is enabled.
|
||||||
|
* triggeredBy is a string that indicates in which case the cached data was requested.
|
||||||
*/
|
*/
|
||||||
getCachedData?: (key: string) => DataT
|
getCachedData?: (key: string, triggeredBy?: GetCachedDataTriggeredBy) => DataT
|
||||||
/**
|
/**
|
||||||
* A function that can be used to alter handler function result after resolving
|
* A function that can be used to alter handler function result after resolving
|
||||||
*/
|
*/
|
||||||
@ -91,7 +93,7 @@ export interface AsyncDataOptions<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AsyncDataExecuteOptions {
|
export interface AsyncDataExecuteOptions {
|
||||||
_initial?: boolean
|
_triggeredBy?: GetCachedDataTriggeredBy
|
||||||
// TODO: remove boolean option in Nuxt 4
|
// TODO: remove boolean option in Nuxt 4
|
||||||
/**
|
/**
|
||||||
* Force a refresh, even if there is already a pending request. Previous requests will
|
* Force a refresh, even if there is already a pending request. Previous requests will
|
||||||
@ -101,7 +103,12 @@ export interface AsyncDataExecuteOptions {
|
|||||||
* Instead of using `boolean` values, use `cancel` for `true` and `defer` for `false`.
|
* Instead of using `boolean` values, use `cancel` for `true` and `defer` for `false`.
|
||||||
* Boolean values will be removed in a future release.
|
* Boolean values will be removed in a future release.
|
||||||
*/
|
*/
|
||||||
dedupe?: boolean | 'cancel' | 'defer'
|
dedupe?: boolean | 'cancel' | 'defer',
|
||||||
|
/**
|
||||||
|
* Do not use potentially cached data from getCachedData and perform a new request
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
force?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface _AsyncData<DataT, ErrorT> {
|
export interface _AsyncData<DataT, ErrorT> {
|
||||||
@ -239,7 +246,7 @@ export function useAsyncData<
|
|||||||
console.warn('[nuxt] `boolean` values are deprecated for the `dedupe` option of `useAsyncData` and will be removed in the future. Use \'cancel\' or \'defer\' instead.')
|
console.warn('[nuxt] `boolean` values are deprecated for the `dedupe` option of `useAsyncData` and will be removed in the future. Use \'cancel\' or \'defer\' instead.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasCachedData = () => ![null, undefined].includes(options.getCachedData!(key) as any)
|
const hasCachedData = (triggeredBy?: GetCachedDataTriggeredBy) => ![null, undefined].includes(options.getCachedData!(key, triggeredBy) as any)
|
||||||
|
|
||||||
// Create or use a shared asyncData entity
|
// Create or use a shared asyncData entity
|
||||||
if (!nuxtApp._asyncData[key] || !options.immediate) {
|
if (!nuxtApp._asyncData[key] || !options.immediate) {
|
||||||
@ -248,8 +255,8 @@ export function useAsyncData<
|
|||||||
const _ref = options.deep ? ref : shallowRef
|
const _ref = options.deep ? ref : shallowRef
|
||||||
|
|
||||||
nuxtApp._asyncData[key] = {
|
nuxtApp._asyncData[key] = {
|
||||||
data: _ref(options.getCachedData!(key) ?? options.default!()),
|
data: _ref(options.getCachedData!(key, 'initial') ?? options.default!()),
|
||||||
pending: ref(!hasCachedData()),
|
pending: ref(!hasCachedData('initial')),
|
||||||
error: toRef(nuxtApp.payload._errors, key),
|
error: toRef(nuxtApp.payload._errors, key),
|
||||||
status: ref('idle')
|
status: ref('idle')
|
||||||
}
|
}
|
||||||
@ -258,7 +265,7 @@ export function useAsyncData<
|
|||||||
// TODO: Else, somehow check for conflicting keys with different defaults or fetcher
|
// TODO: Else, somehow check for conflicting keys with different defaults or fetcher
|
||||||
const asyncData = { ...nuxtApp._asyncData[key] } as AsyncData<DataT | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
|
const asyncData = { ...nuxtApp._asyncData[key] } as AsyncData<DataT | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
|
||||||
|
|
||||||
asyncData.refresh = asyncData.execute = (opts = {}) => {
|
asyncData.refresh = asyncData.execute = (opts = { _triggeredBy: 'refresh:manual' }) => {
|
||||||
if (nuxtApp._asyncDataPromises[key]) {
|
if (nuxtApp._asyncDataPromises[key]) {
|
||||||
if (isDefer(opts.dedupe ?? options.dedupe)) {
|
if (isDefer(opts.dedupe ?? options.dedupe)) {
|
||||||
// Avoid fetching same key more than once at a time
|
// Avoid fetching same key more than once at a time
|
||||||
@ -267,8 +274,8 @@ export function useAsyncData<
|
|||||||
(nuxtApp._asyncDataPromises[key] as any).cancelled = true
|
(nuxtApp._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 || (nuxtApp.isHydrating && opts._initial !== false)) && hasCachedData()) {
|
if (!opts.force && hasCachedData(opts._triggeredBy)) {
|
||||||
return Promise.resolve(options.getCachedData!(key))
|
return Promise.resolve(options.getCachedData!(key, opts._triggeredBy))
|
||||||
}
|
}
|
||||||
asyncData.pending.value = true
|
asyncData.pending.value = true
|
||||||
asyncData.status.value = 'pending'
|
asyncData.status.value = 'pending'
|
||||||
@ -318,7 +325,7 @@ export function useAsyncData<
|
|||||||
return nuxtApp._asyncDataPromises[key]!
|
return nuxtApp._asyncDataPromises[key]!
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialFetch = () => asyncData.refresh({ _initial: true })
|
const initialFetch = () => asyncData.refresh({ _triggeredBy: 'refresh:manual' })
|
||||||
|
|
||||||
const fetchOnServer = options.server !== false && nuxtApp.payload.serverRendered
|
const fetchOnServer = options.server !== false && nuxtApp.payload.serverRendered
|
||||||
|
|
||||||
@ -352,7 +359,7 @@ export function useAsyncData<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fetchOnServer && nuxtApp.isHydrating && (asyncData.error.value || hasCachedData())) {
|
if (fetchOnServer && nuxtApp.isHydrating && (asyncData.error.value || hasCachedData('initial'))) {
|
||||||
// 1. Hydration (server: true): no fetch
|
// 1. Hydration (server: true): no fetch
|
||||||
asyncData.pending.value = false
|
asyncData.pending.value = false
|
||||||
asyncData.status.value = asyncData.error.value ? 'error' : 'success'
|
asyncData.status.value = asyncData.error.value ? 'error' : 'success'
|
||||||
@ -365,11 +372,11 @@ export function useAsyncData<
|
|||||||
initialFetch()
|
initialFetch()
|
||||||
}
|
}
|
||||||
if (options.watch) {
|
if (options.watch) {
|
||||||
watch(options.watch, () => asyncData.refresh())
|
watch(options.watch, () => asyncData.refresh({ _triggeredBy: 'watch'}))
|
||||||
}
|
}
|
||||||
const off = nuxtApp.hook('app:data:refresh', async (keys) => {
|
const off = nuxtApp.hook('app:data:refresh', async (keys, force) => {
|
||||||
if (!keys || keys.includes(key)) {
|
if (!keys || keys.includes(key)) {
|
||||||
await asyncData.refresh()
|
await asyncData.refresh({ force, _triggeredBy: 'refresh:hook' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (instance) {
|
if (instance) {
|
||||||
@ -462,7 +469,7 @@ export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @since 3.0.0 */
|
/** @since 3.0.0 */
|
||||||
export async function refreshNuxtData (keys?: string | string[]): Promise<void> {
|
export async function refreshNuxtData (keys?: string | string[], force?: boolean): Promise<void> {
|
||||||
if (import.meta.server) {
|
if (import.meta.server) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
@ -470,7 +477,7 @@ export async function refreshNuxtData (keys?: string | string[]): Promise<void>
|
|||||||
await new Promise<void>(resolve => onNuxtReady(resolve))
|
await new Promise<void>(resolve => onNuxtReady(resolve))
|
||||||
|
|
||||||
const _keys = keys ? toArray(keys) : undefined
|
const _keys = keys ? toArray(keys) : undefined
|
||||||
await useNuxtApp().hooks.callHookParallel('app:data:refresh', _keys)
|
await useNuxtApp().hooks.callHookParallel('app:data:refresh', _keys, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @since 3.0.0 */
|
/** @since 3.0.0 */
|
||||||
|
@ -38,7 +38,7 @@ export interface RuntimeNuxtHooks {
|
|||||||
'app:error': (err: any) => HookResult
|
'app:error': (err: any) => HookResult
|
||||||
'app:error:cleared': (options: { redirect?: string }) => HookResult
|
'app:error:cleared': (options: { redirect?: string }) => HookResult
|
||||||
'app:chunkError': (options: { error: any }) => HookResult
|
'app:chunkError': (options: { error: any }) => HookResult
|
||||||
'app:data:refresh': (keys?: string[]) => HookResult
|
'app:data:refresh': (keys?: string[], force?: boolean) => HookResult
|
||||||
'app:manifest:update': (meta?: NuxtAppManifestMeta) => HookResult
|
'app:manifest:update': (meta?: NuxtAppManifestMeta) => HookResult
|
||||||
'link:prefetch': (link: string) => HookResult
|
'link:prefetch': (link: string) => HookResult
|
||||||
'page:start': (Component?: VNode) => HookResult
|
'page:start': (Component?: VNode) => HookResult
|
||||||
|
@ -1,13 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup>
|
||||||
|
const page = ref(0)
|
||||||
|
const nuxt = useNuxtApp()
|
||||||
|
const { data, refresh } = await useFetch('https://icanhazdadjoke.com/', {
|
||||||
|
query: { page }, // "fed" into watch array for asyncData under the hood
|
||||||
|
getCachedData: (key) => {
|
||||||
|
return nuxt.payload.data[key] || nuxt.static.data[key]
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Edit this file to play around with Nuxt but never commit changes! -->
|
|
||||||
<div>
|
<div>
|
||||||
Nuxt 3 Playground
|
<button @click="refresh()">
|
||||||
|
New Joke (refresh, default)
|
||||||
|
</button>
|
||||||
|
<button @click="refresh({ force: true })">
|
||||||
|
New Joke (refresh, force)
|
||||||
|
</button>
|
||||||
|
<button @click="page++">
|
||||||
|
New Joke (update query value + 1)
|
||||||
|
</button>
|
||||||
|
<button @click="page--">
|
||||||
|
New Joke (update query value - 1)
|
||||||
|
</button>
|
||||||
|
{{ data.joke }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
|
routeRules: {
|
||||||
|
'/': {
|
||||||
|
headers: {
|
||||||
|
'My-Header': 'My-Value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -222,6 +222,13 @@ describe('useAsyncData', () => {
|
|||||||
expect(data.data.value).toMatchInlineSnapshot('"test"')
|
expect(data.data.value).toMatchInlineSnapshot('"test"')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should be refreshable with force and cache', async () => {
|
||||||
|
await useAsyncData('key', () => Promise.resolve('test'), { getCachedData: () => 'cached' })
|
||||||
|
await refreshNuxtData('key', { force: true })
|
||||||
|
const data = useNuxtData('key')
|
||||||
|
expect(data.data.value).toMatchInlineSnapshot('"test"')
|
||||||
|
})
|
||||||
|
|
||||||
it('allows custom access to a cache', async () => {
|
it('allows custom access to a cache', async () => {
|
||||||
const { data } = await useAsyncData(() => ({ val: true }), { getCachedData: () => ({ val: false }) })
|
const { data } = await useAsyncData(() => ({ val: true }), { getCachedData: () => ({ val: false }) })
|
||||||
expect(data.value).toMatchInlineSnapshot(`
|
expect(data.value).toMatchInlineSnapshot(`
|
||||||
@ -231,6 +238,43 @@ describe('useAsyncData', () => {
|
|||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('will use cache on refresh by default', async () => {
|
||||||
|
let called = 0
|
||||||
|
const fn = () => called++
|
||||||
|
const { data, refresh } = await useAsyncData(() => 'other value', { getCachedData: () => fn() })
|
||||||
|
expect(data.value).toBe(0)
|
||||||
|
await refresh()
|
||||||
|
expect(data.value).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('will not use cache with force option', async () => {
|
||||||
|
let called = 0
|
||||||
|
const fn = () => called++
|
||||||
|
const { data, refresh } = await useAsyncData(() => 'other value', { getCachedData: () => fn() })
|
||||||
|
expect(data.value).toBe(0)
|
||||||
|
await refresh({ force: true })
|
||||||
|
expect(data.value).toBe('other value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getCachedData should receive triggeredBy on initial fetch', async () => {
|
||||||
|
const { data } = await useAsyncData(() => '', { getCachedData: (_, triggeredBy) => triggeredBy })
|
||||||
|
expect(data.value).toBe('initial')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getCachedData should receive triggeredBy on manual refresh', async () => {
|
||||||
|
const { data, refresh } = await useAsyncData(() => '', { getCachedData: (_, triggeredBy) => triggeredBy })
|
||||||
|
await refresh()
|
||||||
|
expect(data.value).toBe('refresh:manual')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getCachedData should receive triggeredBy on watch', async () => {
|
||||||
|
const number = ref(0)
|
||||||
|
const { data } = await useAsyncData(() => '', { getCachedData: (_, triggeredBy) => triggeredBy })
|
||||||
|
number.value = 1
|
||||||
|
// TODO: Maybe setTimeout or similar
|
||||||
|
expect(data.value).toBe('watch')
|
||||||
|
})
|
||||||
|
|
||||||
it('should use default while pending', async () => {
|
it('should use default while pending', async () => {
|
||||||
const promise = useAsyncData(() => Promise.resolve('test'), { default: () => 'default' })
|
const promise = useAsyncData(() => Promise.resolve('test'), { default: () => 'default' })
|
||||||
const { data, pending } = promise
|
const { data, pending } = promise
|
||||||
@ -369,7 +413,7 @@ describe('useHydration', () => {
|
|||||||
it('should hydrate value from payload', async () => {
|
it('should hydrate value from payload', async () => {
|
||||||
let val: any
|
let val: any
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
useHydration('key', () => {}, (fromPayload) => { val = fromPayload })
|
useHydration('key', () => { }, (fromPayload) => { val = fromPayload })
|
||||||
await nuxtApp.hooks.callHook('app:created', nuxtApp.vueApp)
|
await nuxtApp.hooks.callHook('app:created', nuxtApp.vueApp)
|
||||||
expect(val).toMatchInlineSnapshot('undefined')
|
expect(val).toMatchInlineSnapshot('undefined')
|
||||||
|
|
||||||
@ -440,7 +484,7 @@ describe('useId', () => {
|
|||||||
const vals = new Set<string>()
|
const vals = new Set<string>()
|
||||||
for (let index = 0; index < 100; index++) {
|
for (let index = 0; index < 100; index++) {
|
||||||
mount(defineComponent({
|
mount(defineComponent({
|
||||||
setup () {
|
setup() {
|
||||||
const id = useId()
|
const id = useId()
|
||||||
vals.add(id)
|
vals.add(id)
|
||||||
return () => h('div', id)
|
return () => h('div', id)
|
||||||
@ -452,7 +496,7 @@ describe('useId', () => {
|
|||||||
|
|
||||||
it('generates unique ids per-component', () => {
|
it('generates unique ids per-component', () => {
|
||||||
const component = defineComponent({
|
const component = defineComponent({
|
||||||
setup () {
|
setup() {
|
||||||
const id = useId()
|
const id = useId()
|
||||||
return () => h('div', id)
|
return () => h('div', id)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user