mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +00:00
feat(nuxt): refreshCookie
+ experimental CookieStore support (#25198)
This commit is contained in:
parent
c446602529
commit
034d1aaa6f
@ -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**.
|
||||
::
|
||||
|
46
docs/3.api/3.utils/refresh-cookie.md
Normal file
46
docs/3.api/3.utils/refresh-cookie.md
Normal file
@ -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]
|
||||
<script setup lang="ts">
|
||||
const tokenCookie = useCookie('token')
|
||||
|
||||
const login = async (username, password) => {
|
||||
const token = await $fetch('/api/token', { ... }) // Sets `token` cookie on response
|
||||
refreshCookie('token')
|
||||
}
|
||||
|
||||
const loggedIn = computed(() => !!tokenCookie.value)
|
||||
</script>
|
||||
```
|
||||
|
||||
::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
|
||||
```
|
6
packages/nuxt/index.d.ts
vendored
6
packages/nuxt/index.d.ts
vendored
@ -11,6 +11,12 @@ declare global {
|
||||
effectiveType: 'slow-2g' | '2g' | '3g' | '4g'
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
cookieStore?: {
|
||||
onchange: (event: any) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
|
@ -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<CookieSerializeOptions & CookieParseOptions, 'decode' | 'encode'>
|
||||
|
||||
export interface CookieOptions<T = any> extends _CookieOptions {
|
||||
@ -29,6 +32,8 @@ const CookieDefaults = {
|
||||
encode: val => encodeURIComponent(typeof val === 'string' ? val : JSON.stringify(val))
|
||||
} satisfies CookieOptions<any>
|
||||
|
||||
const store = import.meta.client && cookieStore ? window.cookieStore : undefined
|
||||
|
||||
/** @since 3.0.0 */
|
||||
export function useCookie<T = string | null | undefined> (name: string, _opts?: CookieOptions<T> & { readonly?: false }): CookieRef<T>
|
||||
export function useCookie<T = string | null | undefined> (name: string, _opts: CookieOptions<T> & { readonly: true }): Readonly<CookieRef<T>>
|
||||
@ -58,7 +63,7 @@ export function useCookie<T = string | null | undefined> (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<T = string | null | undefined> (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<T = string | null | undefined> (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<T = string | null | undefined> (name: string, _opts?:
|
||||
|
||||
return cookie as CookieRef<T>
|
||||
}
|
||||
/** @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<string, unknown> | undefined {
|
||||
if (import.meta.server) {
|
||||
|
@ -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}`,
|
||||
|
@ -62,7 +62,7 @@ const granularAppPresets: InlinePreset[] = [
|
||||
from: '#app/composables/fetch'
|
||||
},
|
||||
{
|
||||
imports: ['useCookie'],
|
||||
imports: ['useCookie', 'refreshCookie'],
|
||||
from: '#app/composables/cookie'
|
||||
},
|
||||
{
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
7
test/fixtures/basic/pages/cookies.vue
vendored
7
test/fixtures/basic/pages/cookies.vue
vendored
@ -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', {
|
||||
<template>
|
||||
<div>
|
||||
<div>cookies testing page</div>
|
||||
<pre>{{ objectCookie }}</pre>
|
||||
<pre>{{ updated }}</pre>
|
||||
<button @click="objectCookie.foo === 'baz' ? objectCookie.foo = 'bar' : objectCookie.foo = 'baz'">
|
||||
Change cookie
|
||||
</button>
|
||||
<button @click="refreshCookie('updated')">
|
||||
Refresh cookie
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
Loading…
Reference in New Issue
Block a user