diff --git a/docs/2.guide/3.going-further/1.experimental-features.md b/docs/2.guide/3.going-further/1.experimental-features.md
index de1717faaa..1410fb0b84 100644
--- a/docs/2.guide/3.going-further/1.experimental-features.md
+++ b/docs/2.guide/3.going-further/1.experimental-features.md
@@ -374,3 +374,19 @@ import { Buffer } from 'node:buffer'
globalThis.Buffer = globalThis.Buffer || Buffer
```
::
+
+## cookieStore
+
+Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
+
+```ts [nuxt.config.ts]
+export defineNuxtConfig({
+ experimental: {
+ cookieStore: true
+ }
+})
+```
+
+::read-more{icon="i-simple-icons-mdnwebdocs" color="gray" to="https://developer.mozilla.org/en-US/docs/Web/API/CookieStore" target="_blank"}
+Read more about the **CookieStore**.
+::
diff --git a/docs/3.api/3.utils/refresh-cookie.md b/docs/3.api/3.utils/refresh-cookie.md
new file mode 100644
index 0000000000..cd73935647
--- /dev/null
+++ b/docs/3.api/3.utils/refresh-cookie.md
@@ -0,0 +1,46 @@
+---
+title: "refreshCookie"
+description: "Refresh useCookie values manually when a cookie has changed"
+navigation:
+ badge: New
+links:
+ - label: Source
+ icon: i-simple-icons-github
+ to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/cookie.ts
+ size: xs
+---
+
+::callout{icon="i-ph-info-duotone" color="blue"}
+This utility is available since [Nuxt v3.10](/blog/v3-10).
+::
+
+## Purpose
+
+The `refreshCookie` function is designed to refresh cookie value returned by `useCookie`.
+
+This is useful for updating the `useCookie` ref when we know the new cookie value has been set in the browser.
+
+## Usage
+
+```vue [app.vue]
+
+```
+
+::callout{to="/docs/guide/going-further/experimental-features#cookiestore"}
+You can enable experimental `cookieStore` option to automatically refresh `useCookie` value when cookie changes in the browser.
+::
+
+## Type
+
+```ts
+refreshCookie(name: string): void
+```
diff --git a/packages/nuxt/index.d.ts b/packages/nuxt/index.d.ts
index 3135b899a1..2256348248 100644
--- a/packages/nuxt/index.d.ts
+++ b/packages/nuxt/index.d.ts
@@ -11,6 +11,12 @@ declare global {
effectiveType: 'slow-2g' | '2g' | '3g' | '4g'
}
}
+
+ interface Window {
+ cookieStore?: {
+ onchange: (event: any) => void
+ }
+ }
}
export {}
diff --git a/packages/nuxt/src/app/composables/cookie.ts b/packages/nuxt/src/app/composables/cookie.ts
index 5c29fa9084..1e6ee18ea9 100644
--- a/packages/nuxt/src/app/composables/cookie.ts
+++ b/packages/nuxt/src/app/composables/cookie.ts
@@ -10,6 +10,9 @@ import { klona } from 'klona'
import { useNuxtApp } from '../nuxt'
import { useRequestEvent } from './ssr'
+// @ts-expect-error virtual import
+import { cookieStore } from '#build/nuxt.config.mjs'
+
type _CookieOptions = Omit
export interface CookieOptions extends _CookieOptions {
@@ -29,6 +32,8 @@ const CookieDefaults = {
encode: val => encodeURIComponent(typeof val === 'string' ? val : JSON.stringify(val))
} satisfies CookieOptions
+const store = import.meta.client && cookieStore ? window.cookieStore : undefined
+
/** @since 3.0.0 */
export function useCookie (name: string, _opts?: CookieOptions & { readonly?: false }): CookieRef
export function useCookie (name: string, _opts: CookieOptions & { readonly: true }): Readonly>
@@ -58,7 +63,7 @@ export function useCookie (name: string, _opts?:
}
if (import.meta.client) {
- const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`nuxt:cookies:${name}`)
+ const channel = store || typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`nuxt:cookies:${name}`)
const callback = () => {
if (opts.readonly || isEqual(cookie.value, cookies[name])) { return }
writeClientCookie(name, cookie.value, opts as CookieSerializeOptions)
@@ -67,6 +72,13 @@ export function useCookie (name: string, _opts?:
channel?.postMessage(opts.encode(cookie.value as T))
}
+ const handleChange = (data: { value?: any, refresh?: boolean }) => {
+ const value = data.refresh ? readRawCookies(opts)?.[name] : opts.decode(data.value)
+ watchPaused = true
+ cookies[name] = cookie.value = value
+ nextTick(() => { watchPaused = false })
+ }
+
let watchPaused = false
if (getCurrentScope()) {
@@ -77,12 +89,13 @@ export function useCookie (name: string, _opts?:
})
}
- if (channel) {
- channel.onmessage = (event) => {
- watchPaused = true
- cookies[name] = cookie.value = opts.decode(event.data)
- nextTick(() => { watchPaused = false })
+ if (store) {
+ store.onchange = (event) => {
+ const cookie = event.changed.find((c: any) => c.name === name)
+ if (cookie) handleChange({ value: cookie.value })
}
+ } else if (channel) {
+ channel.onmessage = ({ data }) => handleChange(data)
}
if (opts.watch) {
@@ -109,6 +122,12 @@ export function useCookie (name: string, _opts?:
return cookie as CookieRef
}
+/** @since 3.10.0 */
+export function refreshCookie(name: string) {
+ if (store || typeof BroadcastChannel === 'undefined') return
+
+ new BroadcastChannel(`nuxt:cookies:${name}`)?.postMessage({ refresh: true })
+}
function readRawCookies (opts: CookieOptions = {}): Record | undefined {
if (import.meta.server) {
diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts
index a28c320eeb..55ee480e13 100644
--- a/packages/nuxt/src/core/templates.ts
+++ b/packages/nuxt/src/core/templates.ts
@@ -378,6 +378,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
`export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`,
`export const payloadExtraction = ${!!ctx.nuxt.options.experimental.payloadExtraction}`,
+ `export const cookieStore = ${!!ctx.nuxt.options.experimental.cookieStore}`,
`export const appManifest = ${!!ctx.nuxt.options.experimental.appManifest}`,
`export const remoteComponentIslands = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.remoteIsland}`,
`export const selectiveClient = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.selectiveClient}`,
diff --git a/packages/nuxt/src/imports/presets.ts b/packages/nuxt/src/imports/presets.ts
index 8259fa6020..ae1b8f474e 100644
--- a/packages/nuxt/src/imports/presets.ts
+++ b/packages/nuxt/src/imports/presets.ts
@@ -62,7 +62,7 @@ const granularAppPresets: InlinePreset[] = [
from: '#app/composables/fetch'
},
{
- imports: ['useCookie'],
+ imports: ['useCookie', 'refreshCookie'],
from: '#app/composables/cookie'
},
{
diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts
index 2ec9cd0a1e..46b0c1397a 100644
--- a/packages/schema/src/config/experimental.ts
+++ b/packages/schema/src/config/experimental.ts
@@ -287,6 +287,12 @@ export default defineUntypedSchema({
* })
*/
sharedPrerenderData: false,
+
+ /**
+ * Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
+ * @see [CookieStore](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore)
+ */
+ cookieStore: false,
/**
* This allows specifying the default options for core Nuxt components and composables.
diff --git a/test/basic.test.ts b/test/basic.test.ts
index fc6dd4d9f5..14e1f5ec29 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -488,10 +488,14 @@ describe('nuxt composables', () => {
return JSON.parse(decodeURIComponent(raw))
}
expect(await extractCookie()).toEqual({ foo: 'bar' })
- await page.getByRole('button').click()
+ await page.getByText('Change cookie').click()
expect(await extractCookie()).toEqual({ foo: 'baz' })
- await page.getByRole('button').click()
+ await page.getByText('Change cookie').click()
expect(await extractCookie()).toEqual({ foo: 'bar' })
+ await page.evaluate(() => document.cookie = 'updated=foobar')
+ await page.getByText('Refresh cookie').click()
+ const text = await page.innerText('pre')
+ expect(text).toContain('foobar')
await page.close()
})
})
diff --git a/test/fixtures/basic/pages/cookies.vue b/test/fixtures/basic/pages/cookies.vue
index 1875097b11..b4d2dfb67a 100644
--- a/test/fixtures/basic/pages/cookies.vue
+++ b/test/fixtures/basic/pages/cookies.vue
@@ -4,6 +4,8 @@ useCookie('accessed-with-default-value', () => 'default')
useCookie('set').value = 'set'
useCookie('set-to-null').value = null
useCookie('set-to-null-with-default', () => 'default').value = null
+
+const updated = useCookie('updated')
// the next set are all sent by browser
useCookie('browser-accessed-but-not-used')
useCookie('browser-accessed-with-default-value', () => 'default')
@@ -19,9 +21,12 @@ const objectCookie = useCookie('browser-object-default', {
cookies testing page
-
{{ objectCookie }}
+
{{ updated }}
+