diff --git a/docs/content/3.api/1.composables/use-async-data.md b/docs/content/3.api/1.composables/use-async-data.md index 2c9c78a679..52f6adb41b 100644 --- a/docs/content/3.api/1.composables/use-async-data.md +++ b/docs/content/3.api/1.composables/use-async-data.md @@ -14,7 +14,7 @@ function useAsyncData( type AsyncDataOptions = { server?: boolean lazy?: boolean - default?: () => DataT + default?: () => DataT | Ref transform?: (input: DataT) => DataT pick?: string[] watch?: WatchSource[] @@ -41,6 +41,7 @@ type DataT = { * _pick_: only pick specified keys in this array from `handler` function result * _watch_: watch reactive sources to auto refresh * _initialCache_: When set to `false`, will skip payload cache for initial fetch. (defaults to `true`) + * _default_: A function that returns the default value (before the handler function returns its value). Under the hood, `lazy: false` uses `` 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. diff --git a/docs/content/3.api/1.composables/use-cookie.md b/docs/content/3.api/1.composables/use-cookie.md index 7e2e6fd255..2110d14902 100644 --- a/docs/content/3.api/1.composables/use-cookie.md +++ b/docs/content/3.api/1.composables/use-cookie.md @@ -132,6 +132,10 @@ The default decoder is `decodeURIComponent` + [destr](https://github.com/unjs/de be returned as the cookie's value. :: +### `default` + +Specifies a function that returns the cookie's default value. The function can also return a `Ref`. + ## Handling cookies in API routes You can use `useCookie` and `setCookie` from [`h3`](https://github.com/unjs/h3) package to set cookies in server API routes. diff --git a/docs/content/3.api/1.composables/use-state.md b/docs/content/3.api/1.composables/use-state.md index 76ea428bd9..bb78f1a382 100644 --- a/docs/content/3.api/1.composables/use-state.md +++ b/docs/content/3.api/1.composables/use-state.md @@ -1,11 +1,11 @@ # `useState` ```ts -useState(key: string, init?: () => T): Ref +useState(key: string, init?: () => T | Ref): Ref ``` * **key**: A unique key ensuring that data fetching can be properly de-duplicated across requests -* **init**: A function that provides initial value for the state when it's not initiated +* **init**: A function that provides initial value for the state when it's not initiated. This function can also return a `Ref`. * **T**: (typescript only) Specify the type of state ::ReadMore{link="/guide/features/state-management"} diff --git a/packages/nuxt3/src/app/composables/asyncData.ts b/packages/nuxt3/src/app/composables/asyncData.ts index d32e691415..d38608ef4b 100644 --- a/packages/nuxt3/src/app/composables/asyncData.ts +++ b/packages/nuxt3/src/app/composables/asyncData.ts @@ -1,5 +1,6 @@ import { onBeforeMount, onServerPrefetch, onUnmounted, ref, getCurrentInstance, watch } from 'vue' import type { Ref, WatchSource } from 'vue' +import { wrapInRef } from './utils' import { NuxtApp, useNuxtApp } from '#app' export type _Transform = (input: Input) => Output @@ -17,7 +18,7 @@ export interface AsyncDataOptions< > { server?: boolean lazy?: boolean - default?: () => DataT + default?: () => DataT | Ref transform?: Transform pick?: PickKeys watch?: MultiWatchSources @@ -85,7 +86,7 @@ export function useAsyncData< const useInitialCache = () => options.initialCache && nuxt.payload.data[key] !== undefined const asyncData = { - data: ref(nuxt.payload.data[key] ?? options.default()), + data: wrapInRef(nuxt.payload.data[key] ?? options.default()), pending: ref(!useInitialCache()), error: ref(nuxt.payload._errors[key] ?? null) } as AsyncData diff --git a/packages/nuxt3/src/app/composables/cookie.ts b/packages/nuxt3/src/app/composables/cookie.ts index 49eac1298e..d5f7bf1b48 100644 --- a/packages/nuxt3/src/app/composables/cookie.ts +++ b/packages/nuxt3/src/app/composables/cookie.ts @@ -1,9 +1,10 @@ -import { Ref, ref, watch } from 'vue' +import { Ref, watch } from 'vue' import { parse, serialize, CookieParseOptions, CookieSerializeOptions } from 'cookie-es' import { appendHeader } from 'h3' import type { CompatibilityEvent } from 'h3' import destr from 'destr' import { useRequestEvent } from './ssr' +import { wrapInRef } from './utils' import { useNuxtApp } from '#app' type _CookieOptions = Omit @@ -11,7 +12,7 @@ type _CookieOptions = Omit extends _CookieOptions { decode?(value: string): T encode?(value: T): string; - default?: () => T + default?: () => T | Ref } export interface CookieRef extends Ref {} @@ -26,7 +27,7 @@ export function useCookie (name: string, _opts?: CookieOptions): C const opts = { ...CookieDefaults, ..._opts } const cookies = readRawCookies(opts) - const cookie = ref(cookies[name] ?? opts.default?.()) + const cookie = wrapInRef(cookies[name] ?? opts.default?.()) if (process.client) { watch(cookie, () => { writeClientCookie(name, cookie.value, opts as CookieSerializeOptions) }) diff --git a/packages/nuxt3/src/app/composables/state.ts b/packages/nuxt3/src/app/composables/state.ts index 083c90f12d..07ac7ac80a 100644 --- a/packages/nuxt3/src/app/composables/state.ts +++ b/packages/nuxt3/src/app/composables/state.ts @@ -1,4 +1,4 @@ -import { toRef } from 'vue' +import { isRef, toRef } from 'vue' import type { Ref } from 'vue' import { useNuxtApp } from '#app' @@ -8,11 +8,17 @@ import { useNuxtApp } from '#app' * @param key a unique key ensuring that data fetching can be properly de-duplicated across requests * @param init a function that provides initial value for the state when it's not initiated */ -export const useState = (key: string, init?: (() => T)): Ref => { +export const useState = (key: string, init?: (() => T | Ref)): Ref => { const nuxt = useNuxtApp() const state = toRef(nuxt.payload.state, key) if (state.value === undefined && init) { - state.value = init() + const initialValue = init() + if (isRef(initialValue)) { + // vue will unwrap the ref for us + nuxt.payload.state[key] = initialValue + return initialValue as Ref + } + state.value = initialValue } return state } diff --git a/packages/nuxt3/src/app/composables/utils.ts b/packages/nuxt3/src/app/composables/utils.ts new file mode 100644 index 0000000000..1f454328cb --- /dev/null +++ b/packages/nuxt3/src/app/composables/utils.ts @@ -0,0 +1,3 @@ +import { isRef, ref, Ref } from 'vue' + +export const wrapInRef = (value: T | Ref) => isRef(value) ? value : ref(value) diff --git a/test/fixtures/basic/types.ts b/test/fixtures/basic/types.ts index a1c57b51d8..0cf64ae8cc 100644 --- a/test/fixtures/basic/types.ts +++ b/test/fixtures/basic/types.ts @@ -104,3 +104,23 @@ describe('runtimeConfig', () => { expectTypeOf(runtimeConfig.unknown).toMatchTypeOf() }) }) + +describe('composables', () => { + it('allows providing default refs', () => { + expectTypeOf(useState('test', () => ref('hello'))).toMatchTypeOf>() + expectTypeOf(useState('test', () => 'hello')).toMatchTypeOf>() + + expectTypeOf(useCookie('test', { default: () => ref(500) })).toMatchTypeOf>() + expectTypeOf(useCookie('test', { default: () => 500 })).toMatchTypeOf>() + + expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => ref(500) }).data).toMatchTypeOf>() + expectTypeOf(useAsyncData('test', () => Promise.resolve(500), { default: () => 500 }).data).toMatchTypeOf>() + // @ts-expect-error + expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => ref(500) }).data).toMatchTypeOf>() + // @ts-expect-error + expectTypeOf(useAsyncData('test', () => Promise.resolve('500'), { default: () => 500 }).data).toMatchTypeOf>() + + expectTypeOf(useFetch('test', { default: () => ref(500) }).data).toMatchTypeOf>() + expectTypeOf(useFetch('test', { default: () => 500 }).data).toMatchTypeOf>() + }) +})