Skip to content

Commit

Permalink
feat(nuxt3, bridge): useCookie universal composable (nuxt#2085)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Nov 22, 2021
1 parent cc342eb commit 9920181
Show file tree
Hide file tree
Showing 13 changed files with 311 additions and 1 deletion.
156 changes: 156 additions & 0 deletions docs/content/3.docs/1.usage/6-cookies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Cookies

> Nuxt provides SSR-friendly composable to read and write cookies.
## Usage

Within your pages, components, and plugins you can use `useCookie` to create a reactive reference bound to a specific cookie.

```js
const cookie = useCookie(name, options)
```

::alert{icon=👉}
**`useCookie` only works during `setup` or `Lifecycle Hooks`**
::

::alert{icon=😌}
`useCookie` ref will be automatically serialize and deserialized cookie value to JSON.
::

## Example

The example below creates a cookie called counter and if it doesn't exist set a random value. Whenever we update `counter`, the cookie will be updated.

```vue
<template>
<div>
<h1> Counter: {{ counter || '-' }}</h1>
<button @click="counter = null">
reset
</button>
<button @click="counter--">
-
</button>
<button @click="counter++">
+
</button>
</div>
</template>
<script setup>
const counter = useCookie('counter')
counter.value = counter.value || Math.round(Math.random() * 1000)
</script>
```

:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/framework/tree/main/examples/use-cookie?terminal=dev" blank}

## Options

Cookie composable accepts these properties in the options. Use them to modify the behavior of cookies.

Most of the options will be directly passed to [cookie](https://github.com/jshttp/cookie) package.

### `maxAge` / `expires`

**`maxAge`** Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.2).
The given number will be converted to an integer by rounding down. By default, no maximum age is set.

**`expires`**: Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.1).
By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and
will delete it on a condition like exiting a web browser application.

::alert{icon=💡}
**Note:** The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and
`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients obey this,
so if both are set, they should point to the same date and time.eaks!
::

::alert
If neither of `expires` and `maxAge` are set, cookie will be session-only and removed if the user closes their browser.
::

#### `httpOnly`

Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy,
the `HttpOnly` attribute is set, otherwise, it is not. By default, the `HttpOnly` attribute is not set.

::alert{icon=💡}
**Note** be careful when setting this to `true`, as compliant clients will not allow client-side
JavaScript to see the cookie in `document.cookie`.
::

#### `secure`

Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy,
the `Secure` attribute is set, otherwise,it is not. By default, the `Secure` attribute is not set.

::alert{icon=💡}
**Note:** be careful when setting this to `true`, as compliant clients will not send the cookie back to
the server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
::

#### `domain`

Specifies the value for the [`Domain` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3). By default, no
domain is set, and most clients will consider the cookie to apply to only the current domain.

#### `path`

Specifies the value for the [`Path` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4). By default, the path
is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4).

#### `sameSite`

Specifies the `boolean` or `string` to be the value for the [`SameSite` `Set-Cookie` attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).

- `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
- `false` will not set the `SameSite` attribute.
- `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement.
- `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
- `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement.

More information about the different enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7).

#### `encode`

Specifies a function that will be used to encode a cookie's value. Since value of a cookie
has a limited character set (and must be a simple string), this function can be used to encode
a value into a string suited for a cookie's value.

The default encoder is the `JSON.stringify` + `encodeURIComponent`.

#### `decode`

Specifies a function that will be used to decode a cookie's value. Since the value of a cookie
has a limited character set (and must be a simple string), this function can be used to decode
a previously-encoded cookie value into a JavaScript string or other object.

The default decoder is `decodeURIComponent` + [destr](https://github.com/unjs/destr).

::alert{icon=💡}
**Note** if an error is thrown from this function, the original, non-decoded cookie value will
be returned as the cookie's value.
::

## 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.

**Example:**

```js
import { useCookie, setCookie } from 'h3'

export default (req, res) => {
// Reat counter cookie
let counter = useCookie(req, 'counter') || 0

// Increase counter cookie by 1
setCookie(res, 'counter', ++counter)

// Send JSON response
return { counter }
}
```
23 changes: 23 additions & 0 deletions examples/use-cookie/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template>
<div>
<h1> Counter: {{ counter || '-' }}</h1>
<button @click="counter = null">
reset
</button>
<button @click="counter--">
-
</button>
<button @click="counter++">
+
</button>
</div>
</template>

<script setup>
const counter = useCookie('counter')
counter.value = counter.value || Math.round(Math.random() * 1000)
</script>

<style scoped>
button { margin: 10px 5px; }
</style>
4 changes: 4 additions & 0 deletions examples/use-cookie/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineNuxtConfig } from 'nuxt3'

export default defineNuxtConfig({
})
12 changes: 12 additions & 0 deletions examples/use-cookie/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "example-use-cookie",
"private": true,
"devDependencies": {
"nuxt3": "latest"
},
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build",
"start": "node .output/server/index.mjs"
}
}
3 changes: 3 additions & 0 deletions examples/use-cookie/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}
4 changes: 4 additions & 0 deletions packages/bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,21 @@
"@nuxt/nitro": "3.0.0",
"@nuxt/postcss8": "^1.1.3",
"@nuxt/schema": "3.0.0",
"@types/cookie": "^0.4.1",
"@vitejs/plugin-legacy": "^1.6.3",
"@vue/composition-api": "^1.4.0",
"@vueuse/head": "^0.7.2",
"acorn": "^8.6.0",
"consola": "^2.15.3",
"cookie": "^0.4.1",
"defu": "^5.0.0",
"destr": "^1.1.0",
"enhanced-resolve": "^5.8.3",
"estree-walker": "^2.0.2",
"externality": "^0.1.5",
"fs-extra": "^10.0.0",
"globby": "^11.0.4",
"h3": "^0.3.3",
"hash-sum": "^2.0.0",
"magic-string": "^0.25.7",
"mlly": "^0.3.13",
Expand Down
1 change: 1 addition & 0 deletions packages/bridge/src/runtime/composables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useNuxtApp } from './app'

export { useLazyAsyncData } from './asyncData'
export { useLazyFetch } from './fetch'
export { useCookie } from './cookie'

export * from '@vue/composition-api'

Expand Down
1 change: 1 addition & 0 deletions packages/bridge/src/runtime/cookie.ts
4 changes: 4 additions & 0 deletions packages/nuxt3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@
"@nuxt/schema": "3.0.0",
"@nuxt/vite-builder": "3.0.0",
"@nuxt/webpack-builder": "3.0.0",
"@types/cookie": "^0.4.1",
"@vue/reactivity": "^3.2.22",
"@vue/shared": "^3.2.22",
"@vueuse/head": "^0.7.2",
"chokidar": "^3.5.2",
"consola": "^2.15.3",
"cookie": "^0.4.1",
"defu": "^5.0.0",
"destr": "^1.1.0",
"globby": "^11.0.4",
"h3": "^0.3.3",
"hash-sum": "^2.0.0",
"hookable": "^5.0.0",
"ignore": "^5.1.9",
Expand Down
77 changes: 77 additions & 0 deletions packages/nuxt3/src/app/composables/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { ServerResponse } from 'http'
import { Ref, ref, watch } from 'vue'
import type { CookieParseOptions, CookieSerializeOptions } from 'cookie'
import { parse, serialize } from 'cookie'
import { appendHeader } from 'h3'
import type { NuxtApp } from '@nuxt/schema'
import destr from 'destr'
import { useNuxtApp } from '#app'

type _CookieOptions = Omit<CookieSerializeOptions & CookieParseOptions, 'decode' | 'encode'>
export interface CookieOptions<T=any> extends _CookieOptions {
decode?(value: string): T
encode?(value: T): string;
}

export interface CookieRef<T> extends Ref<T> {}

const CookieDefaults: CookieOptions<any> = {
decode: val => destr(decodeURIComponent(val)),
encode: val => encodeURIComponent(typeof val === 'string' ? val : JSON.stringify(val))
}

export function useCookie <T=string> (name: string, _opts: CookieOptions<T>): CookieRef<T> {
const opts = { ...CookieDefaults, ..._opts }
const cookies = readRawCookies(opts)

const cookie = ref(opts.decode(cookies[name]))

if (process.client) {
watch(cookie, () => { writeClientCookie(name, cookie.value, opts) })
} else if (process.server) {
const initialValue = cookie.value
const nuxtApp = useNuxtApp()
nuxtApp.hooks.hookOnce('app:rendered', () => {
if (cookie.value !== initialValue) {
// @ts-ignore
writeServerCookie(useSSRRes(nuxtApp), name, cookie.value, opts)
}
})
}

return cookie
}

// @ts-ignore
function useSSRReq (nuxtApp?: NuxtApp = useNuxtApp()) { return nuxtApp.ssrContext?.req }

// @ts-ignore
function useSSRRes (nuxtApp?: NuxtApp = useNuxtApp()) { return nuxtApp.ssrContext?.res }

function readRawCookies (opts: CookieOptions = {}): Record<string, string> {
if (process.server) {
return parse(useSSRReq().headers.cookie || '', opts)
} else if (process.client) {
return parse(document.cookie, opts)
}
}

function serializeCookie (name: string, value: any, opts: CookieSerializeOptions = {}) {
if (value === null || value === undefined) {
opts.maxAge = -1
}
return serialize(name, value, opts)
}

function writeClientCookie (name: string, value: any, opts: CookieSerializeOptions = {}) {
if (process.client) {
document.cookie = serializeCookie(name, value, opts)
}
}

function writeServerCookie (res: ServerResponse, name: string, value: any, opts: CookieSerializeOptions = {}) {
if (res) {
// TODO: Try to smart join with exisiting Set-Cookie headers
appendHeader(res, 'Set-Cookie', serializeCookie(name, value, opts))
}
}
1 change: 1 addition & 0 deletions packages/nuxt3/src/app/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { useAsyncData, useLazyAsyncData } from './asyncData'
export { useHydration } from './hydrate'
export { useState } from './state'
export { useFetch, useLazyFetch } from './fetch'
export { useCookie } from './cookie'
3 changes: 2 additions & 1 deletion packages/nuxt3/src/auto-imports/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const Nuxt3AutoImports: AutoImportSource[] = [
'useRuntimeConfig',
'useState',
'useFetch',
'useLazyFetch'
'useLazyFetch',
'useCookie'
]
},
// #meta
Expand Down
Loading

0 comments on commit 9920181

Please sign in to comment.