@usefy/use-cookie 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -0
- package/dist/index.d.mts +174 -0
- package/dist/index.d.ts +174 -0
- package/dist/index.js +252 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +222 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/mirunamu00/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@usefy/use-cookie</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Read and write a browser cookie as React state — SSR-aware, JSON-friendly, with full cookie attribute support</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@usefy/use-cookie"><img src="https://img.shields.io/npm/v/@usefy/use-cookie.svg?style=flat-square&color=007acc" alt="npm version" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@usefy/use-cookie"><img src="https://img.shields.io/npm/dm/@usefy/use-cookie.svg?style=flat-square&color=007acc" alt="npm downloads" /></a>
|
|
14
|
+
<a href="https://bundlephobia.com/package/@usefy/use-cookie"><img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-cookie?style=flat-square&color=007acc" alt="bundle size" /></a>
|
|
15
|
+
<a href="https://github.com/mirunamu00/usefy/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@usefy/use-cookie.svg?style=flat-square&color=007acc" alt="license" /></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="#installation">Installation</a> •
|
|
20
|
+
<a href="#quick-start">Quick Start</a> •
|
|
21
|
+
<a href="#api">API</a> •
|
|
22
|
+
<a href="#testing">Testing</a> •
|
|
23
|
+
<a href="#license">License</a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="https://mirunamu00.github.io/usefy/?path=/docs/hooks-usecookie--docs" target="_blank" rel="noopener noreferrer">
|
|
28
|
+
<strong>📚 View Storybook Demo</strong>
|
|
29
|
+
</a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
|
|
36
|
+
`useCookie` is part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It reads and writes a browser cookie with a `useState`-like API, and completes the storage trio alongside [`@usefy/use-local-storage`](https://www.npmjs.com/package/@usefy/use-local-storage) and [`@usefy/use-session-storage`](https://www.npmjs.com/package/@usefy/use-session-storage) — the three feel like siblings.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **`useState`-like tuple** — `[value, setValue, remove]`, with a functional updater `(prev) => next`
|
|
41
|
+
- **Full cookie attributes** — `expires` (Date or days), `maxAge` (seconds), `path`, `domain`, `secure`, `sameSite`
|
|
42
|
+
- **Safe (de)serialization** — JSON by default, with a graceful fallback to the raw string so plain (non-JSON) cookies never throw
|
|
43
|
+
- **SSR-safe** — guards all `document` access and returns `initialValue` on the server (built on `useSyncExternalStore`, exactly like its storage siblings, to avoid hydration mismatches)
|
|
44
|
+
- **Same-document sync** — multiple `useCookie(key)` instances stay in sync when any of them writes or removes
|
|
45
|
+
- **Stable references** — `setValue` and `remove` are memoized
|
|
46
|
+
- **TypeScript-first** — full type inference and exported types
|
|
47
|
+
- **Tiny & tree-shakeable** — zero dependencies, published as its own package
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# npm
|
|
53
|
+
npm install @usefy/use-cookie
|
|
54
|
+
|
|
55
|
+
# yarn
|
|
56
|
+
yarn add @usefy/use-cookie
|
|
57
|
+
|
|
58
|
+
# pnpm
|
|
59
|
+
pnpm add @usefy/use-cookie
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires React 18 or 19 (`peerDependencies: "react": "^18.0.0 || ^19.0.0"`).
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { useCookie } from "@usefy/use-cookie";
|
|
68
|
+
|
|
69
|
+
function ThemeToggle() {
|
|
70
|
+
const [theme, setTheme, removeTheme] = useCookie("theme", {
|
|
71
|
+
initialValue: "light",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div>
|
|
76
|
+
<p>Current theme: {theme}</p>
|
|
77
|
+
<button onClick={() => setTheme("dark")}>Dark</button>
|
|
78
|
+
<button onClick={removeTheme}>Reset</button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
### `useCookie<T>(key, options?)`
|
|
87
|
+
|
|
88
|
+
Reads and writes the cookie named `key`, returning a `[value, setValue, remove]` tuple.
|
|
89
|
+
|
|
90
|
+
#### Parameters
|
|
91
|
+
|
|
92
|
+
| Parameter | Type | Description |
|
|
93
|
+
| --------- | --------------------- | ------------------------- |
|
|
94
|
+
| `key` | `string` | The cookie name |
|
|
95
|
+
| `options` | `UseCookieOptions<T>` | Configuration (see below) |
|
|
96
|
+
|
|
97
|
+
#### Options
|
|
98
|
+
|
|
99
|
+
| Option | Type | Default | Description |
|
|
100
|
+
| -------------- | ------------------------ | ---------------- | ----------------------------------------------------------------------- |
|
|
101
|
+
| `initialValue` | `T \| (() => T)` | `undefined` | Value when the cookie is absent (and on the server); also the reset target for `remove()`. Supports a lazy initializer. |
|
|
102
|
+
| `serializer` | `(value: T) => string` | `JSON.stringify` | Convert the value to the stored cookie string |
|
|
103
|
+
| `deserializer` | `(value: string) => T` | JSON-or-raw\* | Parse the stored cookie string back to a value |
|
|
104
|
+
| `onError` | `(error: Error) => void` | — | Called on a serialization/deserialization error |
|
|
105
|
+
| `expires` | `Date \| number` | — | Absolute `Date`, or a number of **days** from now. Omit for a session cookie. |
|
|
106
|
+
| `maxAge` | `number` | — | `Max-Age` in **seconds**. Use `0`/negative to expire immediately. |
|
|
107
|
+
| `path` | `string` | `"/"` | Path the cookie is scoped to |
|
|
108
|
+
| `domain` | `string` | — | Domain the cookie is scoped to (e.g. `.example.com`) |
|
|
109
|
+
| `secure` | `boolean` | — | Send the cookie over HTTPS only |
|
|
110
|
+
| `sameSite` | `'strict' \| 'lax' \| 'none'` | — | `SameSite` policy. `'none'` requires `secure: true`. |
|
|
111
|
+
|
|
112
|
+
\* **Default deserialization:** the value is written with `JSON.stringify` and read with `JSON.parse`. Because cookies are very often plain, non-JSON strings (set by a server or another library), the default deserializer **falls back to the raw string** when `JSON.parse` throws — so `foo=bar` reads back as `"bar"` and never crashes, while `'{"a":1}'` reads back as `{ a: 1 }`.
|
|
113
|
+
|
|
114
|
+
#### Returns `[T | undefined, SetValue, Remove]`
|
|
115
|
+
|
|
116
|
+
| Index | Type | Description |
|
|
117
|
+
| ----- | ----------------------------------------------- | ------------------------------------------------------------- |
|
|
118
|
+
| `[0]` | `T \| undefined` | Current value (`undefined` when absent and no `initialValue`) |
|
|
119
|
+
| `[1]` | `Dispatch<SetStateAction<T \| undefined>>` | Set the cookie; accepts a value or `(prev) => next` |
|
|
120
|
+
| `[2]` | `() => void` | Delete the cookie and reset state to `initialValue` |
|
|
121
|
+
|
|
122
|
+
### SSR / hydration
|
|
123
|
+
|
|
124
|
+
On the server there is no `document`, so `useCookie` returns `initialValue`. Like `@usefy/use-local-storage`, it reads through `useSyncExternalStore` with a server snapshot, so the first client render is consistent and there is no hydration mismatch.
|
|
125
|
+
|
|
126
|
+
### Cross-tab limitation
|
|
127
|
+
|
|
128
|
+
Cookies have **no `storage` event**, so writes made in *other* browser tabs cannot be observed without polling — `useCookie` does not fake this. It does keep every `useCookie(key)` instance **within the same document** in sync via an internal subscription (mirroring `@usefy/use-local-storage`'s same-tab store). If you need cross-tab reactivity for a cookie, poll `document.cookie` yourself or store the value in `localStorage` instead.
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
|
|
132
|
+
### Cookie attributes
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { useCookie } from "@usefy/use-cookie";
|
|
136
|
+
|
|
137
|
+
// A secure, same-site session token that expires in a week.
|
|
138
|
+
const [token, setToken, clearToken] = useCookie<string>("token", {
|
|
139
|
+
expires: 7, // days (a Date also works)
|
|
140
|
+
path: "/",
|
|
141
|
+
secure: true,
|
|
142
|
+
sameSite: "strict",
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Object value (JSON)
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { useCookie } from "@usefy/use-cookie";
|
|
150
|
+
|
|
151
|
+
interface Prefs {
|
|
152
|
+
lang: string;
|
|
153
|
+
compact: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const [prefs, setPrefs] = useCookie<Prefs>("prefs", {
|
|
157
|
+
initialValue: { lang: "en", compact: false },
|
|
158
|
+
maxAge: 60 * 60 * 24 * 30, // 30 days
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
setPrefs((prev) => ({ ...prev!, compact: true }));
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Reading a plain server-set cookie
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { useCookie } from "@usefy/use-cookie";
|
|
168
|
+
|
|
169
|
+
// Cookie set by the backend as `consent=accepted` (not JSON) — reads back as
|
|
170
|
+
// the raw string, no throw.
|
|
171
|
+
const [consent] = useCookie<string>("consent");
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Testing
|
|
175
|
+
|
|
176
|
+
📊 <a href="https://mirunamu00.github.io/usefy/coverage/use-cookie/src/index.html" target="_blank" rel="noopener noreferrer"><strong>View Detailed Coverage Report</strong></a> (GitHub Pages) — **47 tests**, 96% statement coverage.
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT © [mirunamu](https://github.com/mirunamu00)
|
|
181
|
+
|
|
182
|
+
This package is part of the [usefy](https://github.com/mirunamu00/usefy) monorepo.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, framework-agnostic helpers for reading and writing `document.cookie`.
|
|
3
|
+
* These are unit-tested in isolation and used by {@link useCookie}.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The `SameSite` cookie attribute values.
|
|
7
|
+
*
|
|
8
|
+
* - `"strict"` — cookie is sent only for same-site requests.
|
|
9
|
+
* - `"lax"` — cookie is sent on same-site requests and top-level navigations.
|
|
10
|
+
* - `"none"` — cookie is sent on all requests; **requires `secure: true`**.
|
|
11
|
+
*/
|
|
12
|
+
type SameSite = "strict" | "lax" | "none";
|
|
13
|
+
/**
|
|
14
|
+
* Cookie write attributes, mirroring the standard `Set-Cookie` directives.
|
|
15
|
+
* These control how the cookie is persisted by the browser.
|
|
16
|
+
*/
|
|
17
|
+
interface CookieAttributes {
|
|
18
|
+
/**
|
|
19
|
+
* Expiry of the cookie. Either an absolute `Date`, or a number of **days**
|
|
20
|
+
* from now (e.g. `7` = expires in a week). Omit for a session cookie.
|
|
21
|
+
*/
|
|
22
|
+
expires?: Date | number;
|
|
23
|
+
/**
|
|
24
|
+
* `Max-Age` in **seconds** from now. Takes precedence over `expires` in
|
|
25
|
+
* browsers that support it. Use `0` or a negative number to expire immediately.
|
|
26
|
+
*/
|
|
27
|
+
maxAge?: number;
|
|
28
|
+
/**
|
|
29
|
+
* The URL path the cookie is scoped to.
|
|
30
|
+
* @default "/"
|
|
31
|
+
*/
|
|
32
|
+
path?: string;
|
|
33
|
+
/** The domain the cookie is scoped to (e.g. `.example.com`). */
|
|
34
|
+
domain?: string;
|
|
35
|
+
/** When `true`, the cookie is only sent over HTTPS. */
|
|
36
|
+
secure?: boolean;
|
|
37
|
+
/** The `SameSite` policy. `"none"` requires `secure: true`. */
|
|
38
|
+
sameSite?: SameSite;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse a raw `document.cookie` string and return the **decoded** value for
|
|
42
|
+
* `key`, or `undefined` when the key is absent. Pure — does not touch
|
|
43
|
+
* `document`, so it is trivially testable.
|
|
44
|
+
*
|
|
45
|
+
* @param cookieString - The raw `document.cookie` value (e.g. `"a=1; b=2"`).
|
|
46
|
+
* @param key - The (un-encoded) cookie name to look up.
|
|
47
|
+
*/
|
|
48
|
+
declare function parseCookie(cookieString: string, key: string): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Build a `document.cookie` assignment string for a cookie, encoding the name
|
|
51
|
+
* and value and appending the standard attributes. Pure and testable.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* buildCookieString("theme", "dark", { path: "/", maxAge: 3600 })
|
|
55
|
+
* // => "theme=dark; Path=/; Max-Age=3600"
|
|
56
|
+
*/
|
|
57
|
+
declare function buildCookieString(name: string, value: string, attributes?: CookieAttributes): string;
|
|
58
|
+
/**
|
|
59
|
+
* Build the `document.cookie` assignment string that **deletes** a cookie. The
|
|
60
|
+
* value is emptied and the cookie is expired in the past (`Max-Age=-1` plus an
|
|
61
|
+
* epoch `Expires`). The same `path`/`domain` used to write it must be provided,
|
|
62
|
+
* otherwise the browser treats it as a different cookie and deletion is a no-op.
|
|
63
|
+
*/
|
|
64
|
+
declare function buildRemovalCookieString(name: string, attributes?: Pick<CookieAttributes, "path" | "domain">): string;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Type for the initial value that can be a value or a function returning a value
|
|
68
|
+
* (lazy initialization).
|
|
69
|
+
*/
|
|
70
|
+
type InitialValue<T> = T | (() => T);
|
|
71
|
+
/**
|
|
72
|
+
* Options for the {@link useCookie} hook. Combines serialization/lifecycle
|
|
73
|
+
* options (aligned with `@usefy/use-local-storage`) with the cookie-specific
|
|
74
|
+
* write attributes from {@link CookieAttributes}.
|
|
75
|
+
*/
|
|
76
|
+
interface UseCookieOptions<T> extends CookieAttributes {
|
|
77
|
+
/**
|
|
78
|
+
* Initial value used when the cookie is absent (or on the server). Supports a
|
|
79
|
+
* lazy initializer function. This is also the value the state resets to when
|
|
80
|
+
* {@link UseCookieReturn} `remove()` is called.
|
|
81
|
+
*/
|
|
82
|
+
initialValue?: InitialValue<T>;
|
|
83
|
+
/**
|
|
84
|
+
* Custom serializer for converting the value to the stored cookie string.
|
|
85
|
+
* @default JSON.stringify
|
|
86
|
+
*/
|
|
87
|
+
serializer?: (value: T) => string;
|
|
88
|
+
/**
|
|
89
|
+
* Custom deserializer for parsing the stored cookie string back to a value.
|
|
90
|
+
* The default tries `JSON.parse` and **falls back to the raw string** for
|
|
91
|
+
* non-JSON cookies (so a plain `foo=bar` cookie never throws).
|
|
92
|
+
* @default (value) => { try { return JSON.parse(value) } catch { return value } }
|
|
93
|
+
*/
|
|
94
|
+
deserializer?: (value: string) => T;
|
|
95
|
+
/** Callback invoked when a serialization/deserialization error occurs. */
|
|
96
|
+
onError?: (error: Error) => void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Return type for {@link useCookie} — a tuple similar to `useState`.
|
|
100
|
+
*/
|
|
101
|
+
type UseCookieReturn<T> = readonly [
|
|
102
|
+
/** Current cookie value (`undefined` when the cookie is absent and no `initialValue` was given). */
|
|
103
|
+
T | undefined,
|
|
104
|
+
/** Update the cookie value. Accepts a value or a functional updater `(prev) => next`. */
|
|
105
|
+
React.Dispatch<React.SetStateAction<T | undefined>>,
|
|
106
|
+
/** Delete the cookie and reset state to `initialValue` (or `undefined`). */
|
|
107
|
+
() => void
|
|
108
|
+
];
|
|
109
|
+
/**
|
|
110
|
+
* A hook for reading and writing a browser **cookie** as React state — the
|
|
111
|
+
* cookie sibling of `@usefy/use-local-storage` and `@usefy/use-session-storage`.
|
|
112
|
+
* Works like `useState` but the value is persisted in `document.cookie`.
|
|
113
|
+
*
|
|
114
|
+
* Features:
|
|
115
|
+
* - **`useState`-like tuple**: `[value, setValue, remove]`.
|
|
116
|
+
* - **Same-document sync**: multiple `useCookie(key)` instances stay in sync.
|
|
117
|
+
* - **SSR-safe**: guards all `document` access and returns `initialValue` on the
|
|
118
|
+
* server (uses `useSyncExternalStore`, identical to its storage siblings, to
|
|
119
|
+
* avoid hydration mismatches).
|
|
120
|
+
* - **Cookie attributes**: `expires`, `maxAge`, `path`, `domain`, `secure`,
|
|
121
|
+
* `sameSite`.
|
|
122
|
+
* - **Safe (de)serialization**: JSON by default, with a graceful fallback to the
|
|
123
|
+
* raw string for plain (non-JSON) cookies.
|
|
124
|
+
*
|
|
125
|
+
* > **Cross-tab note:** cookies have no `storage` event, so writes in *other*
|
|
126
|
+
* > tabs are not observed without polling. Same-document instances always sync.
|
|
127
|
+
*
|
|
128
|
+
* @template T - The type of the stored value
|
|
129
|
+
* @param key - The cookie name
|
|
130
|
+
* @param options - Serialization, initial value, and cookie write attributes
|
|
131
|
+
* @returns A tuple of `[value, setValue, remove]`
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```tsx
|
|
135
|
+
* // Basic usage — a string cookie
|
|
136
|
+
* function ThemeToggle() {
|
|
137
|
+
* const [theme, setTheme, removeTheme] = useCookie("theme", {
|
|
138
|
+
* initialValue: "light",
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* return (
|
|
142
|
+
* <div>
|
|
143
|
+
* <p>Current theme: {theme}</p>
|
|
144
|
+
* <button onClick={() => setTheme("dark")}>Dark</button>
|
|
145
|
+
* <button onClick={removeTheme}>Reset</button>
|
|
146
|
+
* </div>
|
|
147
|
+
* );
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```tsx
|
|
153
|
+
* // With cookie attributes — persist for a week over HTTPS only
|
|
154
|
+
* const [token, setToken] = useCookie<string>("token", {
|
|
155
|
+
* expires: 7, // days (or a Date)
|
|
156
|
+
* path: "/",
|
|
157
|
+
* secure: true,
|
|
158
|
+
* sameSite: "strict",
|
|
159
|
+
* });
|
|
160
|
+
* ```
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```tsx
|
|
164
|
+
* // Object value with a functional update (JSON serialized)
|
|
165
|
+
* interface Prefs { lang: string; compact: boolean }
|
|
166
|
+
* const [prefs, setPrefs] = useCookie<Prefs>("prefs", {
|
|
167
|
+
* initialValue: { lang: "en", compact: false },
|
|
168
|
+
* });
|
|
169
|
+
* setPrefs((prev) => ({ ...prev!, compact: true }));
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
declare function useCookie<T = string>(key: string, options?: UseCookieOptions<T>): UseCookieReturn<T>;
|
|
173
|
+
|
|
174
|
+
export { type CookieAttributes, type InitialValue, type SameSite, type UseCookieOptions, type UseCookieReturn, buildCookieString, buildRemovalCookieString, parseCookie, useCookie };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, framework-agnostic helpers for reading and writing `document.cookie`.
|
|
3
|
+
* These are unit-tested in isolation and used by {@link useCookie}.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The `SameSite` cookie attribute values.
|
|
7
|
+
*
|
|
8
|
+
* - `"strict"` — cookie is sent only for same-site requests.
|
|
9
|
+
* - `"lax"` — cookie is sent on same-site requests and top-level navigations.
|
|
10
|
+
* - `"none"` — cookie is sent on all requests; **requires `secure: true`**.
|
|
11
|
+
*/
|
|
12
|
+
type SameSite = "strict" | "lax" | "none";
|
|
13
|
+
/**
|
|
14
|
+
* Cookie write attributes, mirroring the standard `Set-Cookie` directives.
|
|
15
|
+
* These control how the cookie is persisted by the browser.
|
|
16
|
+
*/
|
|
17
|
+
interface CookieAttributes {
|
|
18
|
+
/**
|
|
19
|
+
* Expiry of the cookie. Either an absolute `Date`, or a number of **days**
|
|
20
|
+
* from now (e.g. `7` = expires in a week). Omit for a session cookie.
|
|
21
|
+
*/
|
|
22
|
+
expires?: Date | number;
|
|
23
|
+
/**
|
|
24
|
+
* `Max-Age` in **seconds** from now. Takes precedence over `expires` in
|
|
25
|
+
* browsers that support it. Use `0` or a negative number to expire immediately.
|
|
26
|
+
*/
|
|
27
|
+
maxAge?: number;
|
|
28
|
+
/**
|
|
29
|
+
* The URL path the cookie is scoped to.
|
|
30
|
+
* @default "/"
|
|
31
|
+
*/
|
|
32
|
+
path?: string;
|
|
33
|
+
/** The domain the cookie is scoped to (e.g. `.example.com`). */
|
|
34
|
+
domain?: string;
|
|
35
|
+
/** When `true`, the cookie is only sent over HTTPS. */
|
|
36
|
+
secure?: boolean;
|
|
37
|
+
/** The `SameSite` policy. `"none"` requires `secure: true`. */
|
|
38
|
+
sameSite?: SameSite;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse a raw `document.cookie` string and return the **decoded** value for
|
|
42
|
+
* `key`, or `undefined` when the key is absent. Pure — does not touch
|
|
43
|
+
* `document`, so it is trivially testable.
|
|
44
|
+
*
|
|
45
|
+
* @param cookieString - The raw `document.cookie` value (e.g. `"a=1; b=2"`).
|
|
46
|
+
* @param key - The (un-encoded) cookie name to look up.
|
|
47
|
+
*/
|
|
48
|
+
declare function parseCookie(cookieString: string, key: string): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Build a `document.cookie` assignment string for a cookie, encoding the name
|
|
51
|
+
* and value and appending the standard attributes. Pure and testable.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* buildCookieString("theme", "dark", { path: "/", maxAge: 3600 })
|
|
55
|
+
* // => "theme=dark; Path=/; Max-Age=3600"
|
|
56
|
+
*/
|
|
57
|
+
declare function buildCookieString(name: string, value: string, attributes?: CookieAttributes): string;
|
|
58
|
+
/**
|
|
59
|
+
* Build the `document.cookie` assignment string that **deletes** a cookie. The
|
|
60
|
+
* value is emptied and the cookie is expired in the past (`Max-Age=-1` plus an
|
|
61
|
+
* epoch `Expires`). The same `path`/`domain` used to write it must be provided,
|
|
62
|
+
* otherwise the browser treats it as a different cookie and deletion is a no-op.
|
|
63
|
+
*/
|
|
64
|
+
declare function buildRemovalCookieString(name: string, attributes?: Pick<CookieAttributes, "path" | "domain">): string;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Type for the initial value that can be a value or a function returning a value
|
|
68
|
+
* (lazy initialization).
|
|
69
|
+
*/
|
|
70
|
+
type InitialValue<T> = T | (() => T);
|
|
71
|
+
/**
|
|
72
|
+
* Options for the {@link useCookie} hook. Combines serialization/lifecycle
|
|
73
|
+
* options (aligned with `@usefy/use-local-storage`) with the cookie-specific
|
|
74
|
+
* write attributes from {@link CookieAttributes}.
|
|
75
|
+
*/
|
|
76
|
+
interface UseCookieOptions<T> extends CookieAttributes {
|
|
77
|
+
/**
|
|
78
|
+
* Initial value used when the cookie is absent (or on the server). Supports a
|
|
79
|
+
* lazy initializer function. This is also the value the state resets to when
|
|
80
|
+
* {@link UseCookieReturn} `remove()` is called.
|
|
81
|
+
*/
|
|
82
|
+
initialValue?: InitialValue<T>;
|
|
83
|
+
/**
|
|
84
|
+
* Custom serializer for converting the value to the stored cookie string.
|
|
85
|
+
* @default JSON.stringify
|
|
86
|
+
*/
|
|
87
|
+
serializer?: (value: T) => string;
|
|
88
|
+
/**
|
|
89
|
+
* Custom deserializer for parsing the stored cookie string back to a value.
|
|
90
|
+
* The default tries `JSON.parse` and **falls back to the raw string** for
|
|
91
|
+
* non-JSON cookies (so a plain `foo=bar` cookie never throws).
|
|
92
|
+
* @default (value) => { try { return JSON.parse(value) } catch { return value } }
|
|
93
|
+
*/
|
|
94
|
+
deserializer?: (value: string) => T;
|
|
95
|
+
/** Callback invoked when a serialization/deserialization error occurs. */
|
|
96
|
+
onError?: (error: Error) => void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Return type for {@link useCookie} — a tuple similar to `useState`.
|
|
100
|
+
*/
|
|
101
|
+
type UseCookieReturn<T> = readonly [
|
|
102
|
+
/** Current cookie value (`undefined` when the cookie is absent and no `initialValue` was given). */
|
|
103
|
+
T | undefined,
|
|
104
|
+
/** Update the cookie value. Accepts a value or a functional updater `(prev) => next`. */
|
|
105
|
+
React.Dispatch<React.SetStateAction<T | undefined>>,
|
|
106
|
+
/** Delete the cookie and reset state to `initialValue` (or `undefined`). */
|
|
107
|
+
() => void
|
|
108
|
+
];
|
|
109
|
+
/**
|
|
110
|
+
* A hook for reading and writing a browser **cookie** as React state — the
|
|
111
|
+
* cookie sibling of `@usefy/use-local-storage` and `@usefy/use-session-storage`.
|
|
112
|
+
* Works like `useState` but the value is persisted in `document.cookie`.
|
|
113
|
+
*
|
|
114
|
+
* Features:
|
|
115
|
+
* - **`useState`-like tuple**: `[value, setValue, remove]`.
|
|
116
|
+
* - **Same-document sync**: multiple `useCookie(key)` instances stay in sync.
|
|
117
|
+
* - **SSR-safe**: guards all `document` access and returns `initialValue` on the
|
|
118
|
+
* server (uses `useSyncExternalStore`, identical to its storage siblings, to
|
|
119
|
+
* avoid hydration mismatches).
|
|
120
|
+
* - **Cookie attributes**: `expires`, `maxAge`, `path`, `domain`, `secure`,
|
|
121
|
+
* `sameSite`.
|
|
122
|
+
* - **Safe (de)serialization**: JSON by default, with a graceful fallback to the
|
|
123
|
+
* raw string for plain (non-JSON) cookies.
|
|
124
|
+
*
|
|
125
|
+
* > **Cross-tab note:** cookies have no `storage` event, so writes in *other*
|
|
126
|
+
* > tabs are not observed without polling. Same-document instances always sync.
|
|
127
|
+
*
|
|
128
|
+
* @template T - The type of the stored value
|
|
129
|
+
* @param key - The cookie name
|
|
130
|
+
* @param options - Serialization, initial value, and cookie write attributes
|
|
131
|
+
* @returns A tuple of `[value, setValue, remove]`
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```tsx
|
|
135
|
+
* // Basic usage — a string cookie
|
|
136
|
+
* function ThemeToggle() {
|
|
137
|
+
* const [theme, setTheme, removeTheme] = useCookie("theme", {
|
|
138
|
+
* initialValue: "light",
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* return (
|
|
142
|
+
* <div>
|
|
143
|
+
* <p>Current theme: {theme}</p>
|
|
144
|
+
* <button onClick={() => setTheme("dark")}>Dark</button>
|
|
145
|
+
* <button onClick={removeTheme}>Reset</button>
|
|
146
|
+
* </div>
|
|
147
|
+
* );
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```tsx
|
|
153
|
+
* // With cookie attributes — persist for a week over HTTPS only
|
|
154
|
+
* const [token, setToken] = useCookie<string>("token", {
|
|
155
|
+
* expires: 7, // days (or a Date)
|
|
156
|
+
* path: "/",
|
|
157
|
+
* secure: true,
|
|
158
|
+
* sameSite: "strict",
|
|
159
|
+
* });
|
|
160
|
+
* ```
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```tsx
|
|
164
|
+
* // Object value with a functional update (JSON serialized)
|
|
165
|
+
* interface Prefs { lang: string; compact: boolean }
|
|
166
|
+
* const [prefs, setPrefs] = useCookie<Prefs>("prefs", {
|
|
167
|
+
* initialValue: { lang: "en", compact: false },
|
|
168
|
+
* });
|
|
169
|
+
* setPrefs((prev) => ({ ...prev!, compact: true }));
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
declare function useCookie<T = string>(key: string, options?: UseCookieOptions<T>): UseCookieReturn<T>;
|
|
173
|
+
|
|
174
|
+
export { type CookieAttributes, type InitialValue, type SameSite, type UseCookieOptions, type UseCookieReturn, buildCookieString, buildRemovalCookieString, parseCookie, useCookie };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
buildCookieString: () => buildCookieString,
|
|
24
|
+
buildRemovalCookieString: () => buildRemovalCookieString,
|
|
25
|
+
parseCookie: () => parseCookie,
|
|
26
|
+
useCookie: () => useCookie
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/useCookie.ts
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
|
|
33
|
+
// src/store.ts
|
|
34
|
+
var listeners = /* @__PURE__ */ new Map();
|
|
35
|
+
function subscribe(key, listener) {
|
|
36
|
+
if (!listeners.has(key)) {
|
|
37
|
+
listeners.set(key, /* @__PURE__ */ new Set());
|
|
38
|
+
}
|
|
39
|
+
const keyListeners = listeners.get(key);
|
|
40
|
+
keyListeners.add(listener);
|
|
41
|
+
return () => {
|
|
42
|
+
keyListeners.delete(listener);
|
|
43
|
+
if (keyListeners.size === 0) {
|
|
44
|
+
listeners.delete(key);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function notifyListeners(key) {
|
|
49
|
+
const keyListeners = listeners.get(key);
|
|
50
|
+
if (keyListeners) {
|
|
51
|
+
keyListeners.forEach((listener) => listener());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/utils.ts
|
|
56
|
+
function isDocumentAvailable() {
|
|
57
|
+
return typeof document !== "undefined";
|
|
58
|
+
}
|
|
59
|
+
function capitalizeSameSite(value) {
|
|
60
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
61
|
+
}
|
|
62
|
+
function defaultSerializer(value) {
|
|
63
|
+
return JSON.stringify(value);
|
|
64
|
+
}
|
|
65
|
+
function defaultDeserializer(value) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(value);
|
|
68
|
+
} catch {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function parseCookie(cookieString, key) {
|
|
73
|
+
if (!cookieString) return void 0;
|
|
74
|
+
const pairs = cookieString.split("; ");
|
|
75
|
+
for (const pair of pairs) {
|
|
76
|
+
const eqIndex = pair.indexOf("=");
|
|
77
|
+
const rawName = eqIndex > -1 ? pair.slice(0, eqIndex) : pair;
|
|
78
|
+
let name;
|
|
79
|
+
try {
|
|
80
|
+
name = decodeURIComponent(rawName);
|
|
81
|
+
} catch {
|
|
82
|
+
name = rawName;
|
|
83
|
+
}
|
|
84
|
+
if (name === key) {
|
|
85
|
+
const rawValue = eqIndex > -1 ? pair.slice(eqIndex + 1) : "";
|
|
86
|
+
try {
|
|
87
|
+
return decodeURIComponent(rawValue);
|
|
88
|
+
} catch {
|
|
89
|
+
return rawValue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
function readRawCookie(key) {
|
|
96
|
+
if (!isDocumentAvailable()) return void 0;
|
|
97
|
+
return parseCookie(document.cookie, key);
|
|
98
|
+
}
|
|
99
|
+
function buildCookieString(name, value, attributes = {}) {
|
|
100
|
+
const { expires, maxAge, path = "/", domain, secure, sameSite } = attributes;
|
|
101
|
+
let str = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
102
|
+
if (expires !== void 0) {
|
|
103
|
+
const date = typeof expires === "number" ? new Date(Date.now() + expires * 864e5) : expires;
|
|
104
|
+
str += `; Expires=${date.toUTCString()}`;
|
|
105
|
+
}
|
|
106
|
+
if (maxAge !== void 0) {
|
|
107
|
+
str += `; Max-Age=${maxAge}`;
|
|
108
|
+
}
|
|
109
|
+
if (path) {
|
|
110
|
+
str += `; Path=${path}`;
|
|
111
|
+
}
|
|
112
|
+
if (domain) {
|
|
113
|
+
str += `; Domain=${domain}`;
|
|
114
|
+
}
|
|
115
|
+
if (secure) {
|
|
116
|
+
str += "; Secure";
|
|
117
|
+
}
|
|
118
|
+
if (sameSite) {
|
|
119
|
+
str += `; SameSite=${capitalizeSameSite(sameSite)}`;
|
|
120
|
+
}
|
|
121
|
+
return str;
|
|
122
|
+
}
|
|
123
|
+
function buildRemovalCookieString(name, attributes = {}) {
|
|
124
|
+
const { path = "/", domain } = attributes;
|
|
125
|
+
let str = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=-1`;
|
|
126
|
+
if (path) {
|
|
127
|
+
str += `; Path=${path}`;
|
|
128
|
+
}
|
|
129
|
+
if (domain) {
|
|
130
|
+
str += `; Domain=${domain}`;
|
|
131
|
+
}
|
|
132
|
+
return str;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/useCookie.ts
|
|
136
|
+
function resolveInitialValue(initialValue) {
|
|
137
|
+
return typeof initialValue === "function" ? initialValue() : initialValue;
|
|
138
|
+
}
|
|
139
|
+
function useCookie(key, options = {}) {
|
|
140
|
+
const {
|
|
141
|
+
initialValue,
|
|
142
|
+
serializer = defaultSerializer,
|
|
143
|
+
deserializer = defaultDeserializer,
|
|
144
|
+
onError,
|
|
145
|
+
expires,
|
|
146
|
+
maxAge,
|
|
147
|
+
path,
|
|
148
|
+
domain,
|
|
149
|
+
secure,
|
|
150
|
+
sameSite
|
|
151
|
+
} = options;
|
|
152
|
+
const serializerRef = (0, import_react.useRef)(serializer);
|
|
153
|
+
const deserializerRef = (0, import_react.useRef)(deserializer);
|
|
154
|
+
const onErrorRef = (0, import_react.useRef)(onError);
|
|
155
|
+
const initialValueRef = (0, import_react.useRef)(initialValue);
|
|
156
|
+
serializerRef.current = serializer;
|
|
157
|
+
deserializerRef.current = deserializer;
|
|
158
|
+
onErrorRef.current = onError;
|
|
159
|
+
initialValueRef.current = initialValue;
|
|
160
|
+
const attributesRef = (0, import_react.useRef)({});
|
|
161
|
+
attributesRef.current = { expires, maxAge, path, domain, secure, sameSite };
|
|
162
|
+
const cacheRef = (0, import_react.useRef)(null);
|
|
163
|
+
const isClient = isDocumentAvailable();
|
|
164
|
+
const subscribeToStore = (0, import_react.useCallback)(
|
|
165
|
+
(onStoreChange) => {
|
|
166
|
+
return subscribe(key, onStoreChange);
|
|
167
|
+
},
|
|
168
|
+
[key]
|
|
169
|
+
);
|
|
170
|
+
const getSnapshot = (0, import_react.useCallback)(() => {
|
|
171
|
+
if (!isClient) {
|
|
172
|
+
return resolveInitialValue(initialValueRef.current);
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const rawValue = readRawCookie(key);
|
|
176
|
+
if (cacheRef.current && cacheRef.current.rawValue === rawValue) {
|
|
177
|
+
return cacheRef.current.parsedValue;
|
|
178
|
+
}
|
|
179
|
+
let parsedValue;
|
|
180
|
+
if (rawValue !== void 0) {
|
|
181
|
+
parsedValue = deserializerRef.current(rawValue);
|
|
182
|
+
} else {
|
|
183
|
+
parsedValue = resolveInitialValue(initialValueRef.current);
|
|
184
|
+
}
|
|
185
|
+
cacheRef.current = { rawValue, parsedValue };
|
|
186
|
+
return parsedValue;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
onErrorRef.current?.(error);
|
|
189
|
+
const fallbackValue = resolveInitialValue(initialValueRef.current);
|
|
190
|
+
cacheRef.current = { rawValue: void 0, parsedValue: fallbackValue };
|
|
191
|
+
return fallbackValue;
|
|
192
|
+
}
|
|
193
|
+
}, [key, isClient]);
|
|
194
|
+
const getServerSnapshot = (0, import_react.useCallback)(() => {
|
|
195
|
+
return resolveInitialValue(initialValueRef.current);
|
|
196
|
+
}, []);
|
|
197
|
+
const storedValue = (0, import_react.useSyncExternalStore)(
|
|
198
|
+
subscribeToStore,
|
|
199
|
+
getSnapshot,
|
|
200
|
+
getServerSnapshot
|
|
201
|
+
);
|
|
202
|
+
const setValue = (0, import_react.useCallback)(
|
|
203
|
+
(value) => {
|
|
204
|
+
try {
|
|
205
|
+
if (!isDocumentAvailable()) return;
|
|
206
|
+
const currentValue = (() => {
|
|
207
|
+
try {
|
|
208
|
+
const raw = readRawCookie(key);
|
|
209
|
+
if (raw !== void 0) {
|
|
210
|
+
return deserializerRef.current(raw);
|
|
211
|
+
}
|
|
212
|
+
return resolveInitialValue(initialValueRef.current);
|
|
213
|
+
} catch {
|
|
214
|
+
return resolveInitialValue(initialValueRef.current);
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
const valueToStore = value instanceof Function ? value(currentValue) : value;
|
|
218
|
+
const serialized = serializerRef.current(valueToStore);
|
|
219
|
+
document.cookie = buildCookieString(
|
|
220
|
+
key,
|
|
221
|
+
serialized,
|
|
222
|
+
attributesRef.current
|
|
223
|
+
);
|
|
224
|
+
cacheRef.current = { rawValue: serialized, parsedValue: valueToStore };
|
|
225
|
+
notifyListeners(key);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
onErrorRef.current?.(error);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
[key]
|
|
231
|
+
);
|
|
232
|
+
const removeValue = (0, import_react.useCallback)(() => {
|
|
233
|
+
try {
|
|
234
|
+
if (!isDocumentAvailable()) return;
|
|
235
|
+
document.cookie = buildRemovalCookieString(key, attributesRef.current);
|
|
236
|
+
const initialVal = resolveInitialValue(initialValueRef.current);
|
|
237
|
+
cacheRef.current = { rawValue: void 0, parsedValue: initialVal };
|
|
238
|
+
notifyListeners(key);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
onErrorRef.current?.(error);
|
|
241
|
+
}
|
|
242
|
+
}, [key]);
|
|
243
|
+
return [storedValue, setValue, removeValue];
|
|
244
|
+
}
|
|
245
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
246
|
+
0 && (module.exports = {
|
|
247
|
+
buildCookieString,
|
|
248
|
+
buildRemovalCookieString,
|
|
249
|
+
parseCookie,
|
|
250
|
+
useCookie
|
|
251
|
+
});
|
|
252
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useCookie.ts","../src/store.ts","../src/utils.ts"],"sourcesContent":["export {\n useCookie,\n type UseCookieOptions,\n type UseCookieReturn,\n type InitialValue,\n type CookieAttributes,\n type SameSite,\n} from \"./useCookie\";\n\nexport {\n buildCookieString,\n buildRemovalCookieString,\n parseCookie,\n} from \"./utils\";\n","import { useCallback, useRef, useSyncExternalStore } from \"react\";\nimport { subscribe, notifyListeners } from \"./store\";\nimport {\n buildCookieString,\n buildRemovalCookieString,\n defaultDeserializer,\n defaultSerializer,\n isDocumentAvailable,\n readRawCookie,\n type CookieAttributes,\n} from \"./utils\";\n\nexport type { CookieAttributes, SameSite } from \"./utils\";\n\n/**\n * Type for the initial value that can be a value or a function returning a value\n * (lazy initialization).\n */\nexport type InitialValue<T> = T | (() => T);\n\n/**\n * Options for the {@link useCookie} hook. Combines serialization/lifecycle\n * options (aligned with `@usefy/use-local-storage`) with the cookie-specific\n * write attributes from {@link CookieAttributes}.\n */\nexport interface UseCookieOptions<T> extends CookieAttributes {\n /**\n * Initial value used when the cookie is absent (or on the server). Supports a\n * lazy initializer function. This is also the value the state resets to when\n * {@link UseCookieReturn} `remove()` is called.\n */\n initialValue?: InitialValue<T>;\n /**\n * Custom serializer for converting the value to the stored cookie string.\n * @default JSON.stringify\n */\n serializer?: (value: T) => string;\n /**\n * Custom deserializer for parsing the stored cookie string back to a value.\n * The default tries `JSON.parse` and **falls back to the raw string** for\n * non-JSON cookies (so a plain `foo=bar` cookie never throws).\n * @default (value) => { try { return JSON.parse(value) } catch { return value } }\n */\n deserializer?: (value: string) => T;\n /** Callback invoked when a serialization/deserialization error occurs. */\n onError?: (error: Error) => void;\n}\n\n/**\n * Return type for {@link useCookie} — a tuple similar to `useState`.\n */\nexport type UseCookieReturn<T> = readonly [\n /** Current cookie value (`undefined` when the cookie is absent and no `initialValue` was given). */\n T | undefined,\n /** Update the cookie value. Accepts a value or a functional updater `(prev) => next`. */\n React.Dispatch<React.SetStateAction<T | undefined>>,\n /** Delete the cookie and reset state to `initialValue` (or `undefined`). */\n () => void\n];\n\n/**\n * Resolve an initial value, supporting a lazy initializer function.\n */\nfunction resolveInitialValue<T>(\n initialValue: InitialValue<T> | undefined\n): T | undefined {\n return typeof initialValue === \"function\"\n ? (initialValue as () => T)()\n : initialValue;\n}\n\n/**\n * A hook for reading and writing a browser **cookie** as React state — the\n * cookie sibling of `@usefy/use-local-storage` and `@usefy/use-session-storage`.\n * Works like `useState` but the value is persisted in `document.cookie`.\n *\n * Features:\n * - **`useState`-like tuple**: `[value, setValue, remove]`.\n * - **Same-document sync**: multiple `useCookie(key)` instances stay in sync.\n * - **SSR-safe**: guards all `document` access and returns `initialValue` on the\n * server (uses `useSyncExternalStore`, identical to its storage siblings, to\n * avoid hydration mismatches).\n * - **Cookie attributes**: `expires`, `maxAge`, `path`, `domain`, `secure`,\n * `sameSite`.\n * - **Safe (de)serialization**: JSON by default, with a graceful fallback to the\n * raw string for plain (non-JSON) cookies.\n *\n * > **Cross-tab note:** cookies have no `storage` event, so writes in *other*\n * > tabs are not observed without polling. Same-document instances always sync.\n *\n * @template T - The type of the stored value\n * @param key - The cookie name\n * @param options - Serialization, initial value, and cookie write attributes\n * @returns A tuple of `[value, setValue, remove]`\n *\n * @example\n * ```tsx\n * // Basic usage — a string cookie\n * function ThemeToggle() {\n * const [theme, setTheme, removeTheme] = useCookie(\"theme\", {\n * initialValue: \"light\",\n * });\n *\n * return (\n * <div>\n * <p>Current theme: {theme}</p>\n * <button onClick={() => setTheme(\"dark\")}>Dark</button>\n * <button onClick={removeTheme}>Reset</button>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With cookie attributes — persist for a week over HTTPS only\n * const [token, setToken] = useCookie<string>(\"token\", {\n * expires: 7, // days (or a Date)\n * path: \"/\",\n * secure: true,\n * sameSite: \"strict\",\n * });\n * ```\n *\n * @example\n * ```tsx\n * // Object value with a functional update (JSON serialized)\n * interface Prefs { lang: string; compact: boolean }\n * const [prefs, setPrefs] = useCookie<Prefs>(\"prefs\", {\n * initialValue: { lang: \"en\", compact: false },\n * });\n * setPrefs((prev) => ({ ...prev!, compact: true }));\n * ```\n */\nexport function useCookie<T = string>(\n key: string,\n options: UseCookieOptions<T> = {}\n): UseCookieReturn<T> {\n const {\n initialValue,\n serializer = defaultSerializer,\n deserializer = defaultDeserializer,\n onError,\n expires,\n maxAge,\n path,\n domain,\n secure,\n sameSite,\n } = options;\n\n // Store options in refs for stable references and access to the latest values.\n const serializerRef = useRef(serializer);\n const deserializerRef = useRef(deserializer);\n const onErrorRef = useRef(onError);\n const initialValueRef = useRef(initialValue);\n\n serializerRef.current = serializer;\n deserializerRef.current = deserializer;\n onErrorRef.current = onError;\n initialValueRef.current = initialValue;\n\n // Latest cookie write attributes, so setValue/remove stay stable across renders.\n const attributesRef = useRef<CookieAttributes>({});\n attributesRef.current = { expires, maxAge, path, domain, secure, sameSite };\n\n // Cache for getSnapshot so it returns a stable reference when the raw cookie\n // value is unchanged — required by useSyncExternalStore to avoid loops.\n const cacheRef = useRef<{\n rawValue: string | undefined;\n parsedValue: T | undefined;\n } | null>(null);\n\n const isClient = isDocumentAvailable();\n\n // Subscribe to same-document changes via the internal store.\n const subscribeToStore = useCallback(\n (onStoreChange: () => void) => {\n return subscribe(key, onStoreChange);\n },\n [key]\n );\n\n // getSnapshot: read the current value from document.cookie with caching.\n const getSnapshot = useCallback((): T | undefined => {\n if (!isClient) {\n return resolveInitialValue(initialValueRef.current);\n }\n\n try {\n const rawValue = readRawCookie(key);\n\n if (cacheRef.current && cacheRef.current.rawValue === rawValue) {\n return cacheRef.current.parsedValue;\n }\n\n let parsedValue: T | undefined;\n if (rawValue !== undefined) {\n parsedValue = deserializerRef.current(rawValue);\n } else {\n parsedValue = resolveInitialValue(initialValueRef.current);\n }\n\n cacheRef.current = { rawValue, parsedValue };\n return parsedValue;\n } catch (error) {\n onErrorRef.current?.(error as Error);\n const fallbackValue = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: undefined, parsedValue: fallbackValue };\n return fallbackValue;\n }\n }, [key, isClient]);\n\n // getServerSnapshot: return the initial value for SSR.\n const getServerSnapshot = useCallback((): T | undefined => {\n return resolveInitialValue(initialValueRef.current);\n }, []);\n\n const storedValue = useSyncExternalStore(\n subscribeToStore,\n getSnapshot,\n getServerSnapshot\n );\n\n // setValue — stable reference that writes the cookie and notifies listeners.\n const setValue = useCallback<React.Dispatch<React.SetStateAction<T | undefined>>>(\n (value) => {\n try {\n if (!isDocumentAvailable()) return;\n\n // Resolve the current value for functional updates.\n const currentValue = (() => {\n try {\n const raw = readRawCookie(key);\n if (raw !== undefined) {\n return deserializerRef.current(raw);\n }\n return resolveInitialValue(initialValueRef.current);\n } catch {\n return resolveInitialValue(initialValueRef.current);\n }\n })();\n\n const valueToStore =\n value instanceof Function ? value(currentValue) : value;\n\n const serialized = serializerRef.current(valueToStore as T);\n document.cookie = buildCookieString(\n key,\n serialized,\n attributesRef.current\n );\n\n // Invalidate cache so the next getSnapshot reads the fresh value.\n cacheRef.current = { rawValue: serialized, parsedValue: valueToStore };\n\n notifyListeners(key);\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n },\n [key]\n );\n\n // remove — stable reference that deletes the cookie and resets to initial.\n const removeValue = useCallback(() => {\n try {\n if (!isDocumentAvailable()) return;\n\n document.cookie = buildRemovalCookieString(key, attributesRef.current);\n\n const initialVal = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: undefined, parsedValue: initialVal };\n\n notifyListeners(key);\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n }, [key]);\n\n return [storedValue, setValue, removeValue] as const;\n}\n","/**\n * Internal Store Manager for cookie synchronization.\n * This module manages listeners for same-document synchronization across\n * components using the same cookie key.\n *\n * Unlike `localStorage`, cookies have no native `storage` event, so there is no\n * way to observe cross-tab writes without polling. This store therefore keeps\n * every `useCookie(key)` instance **within the same document** in sync: when one\n * instance writes or removes a cookie it notifies the others. Cross-tab\n * synchronization is intentionally not attempted (see the README).\n *\n * @internal This module is not exported publicly\n */\n\n/** Map of key -> Set of listener callbacks */\nconst listeners = new Map<string, Set<() => void>>();\n\n/**\n * Subscribe a listener to changes for a specific key\n * @param key - The cookie key to subscribe to\n * @param listener - Callback to invoke when the key's value changes\n * @returns Unsubscribe function\n */\nexport function subscribe(key: string, listener: () => void): () => void {\n if (!listeners.has(key)) {\n listeners.set(key, new Set());\n }\n\n const keyListeners = listeners.get(key)!;\n keyListeners.add(listener);\n\n return () => {\n keyListeners.delete(listener);\n\n // Cleanup: remove the key entry if no more listeners\n if (keyListeners.size === 0) {\n listeners.delete(key);\n }\n };\n}\n\n/**\n * Notify all listeners subscribed to a specific key.\n * This is called when setValue or removeValue is invoked to synchronize all\n * components using the same key in the same document.\n *\n * @param key - The cookie key that was updated\n */\nexport function notifyListeners(key: string): void {\n const keyListeners = listeners.get(key);\n if (keyListeners) {\n keyListeners.forEach((listener) => listener());\n }\n}\n\n/**\n * Get the count of listeners for a key (for testing purposes)\n * @internal\n */\nexport function getListenerCount(key: string): number {\n return listeners.get(key)?.size ?? 0;\n}\n\n/**\n * Clear all listeners (for testing purposes)\n * @internal\n */\nexport function clearAllListeners(): void {\n listeners.clear();\n}\n","/**\n * Pure, framework-agnostic helpers for reading and writing `document.cookie`.\n * These are unit-tested in isolation and used by {@link useCookie}.\n */\n\n/**\n * The `SameSite` cookie attribute values.\n *\n * - `\"strict\"` — cookie is sent only for same-site requests.\n * - `\"lax\"` — cookie is sent on same-site requests and top-level navigations.\n * - `\"none\"` — cookie is sent on all requests; **requires `secure: true`**.\n */\nexport type SameSite = \"strict\" | \"lax\" | \"none\";\n\n/**\n * Cookie write attributes, mirroring the standard `Set-Cookie` directives.\n * These control how the cookie is persisted by the browser.\n */\nexport interface CookieAttributes {\n /**\n * Expiry of the cookie. Either an absolute `Date`, or a number of **days**\n * from now (e.g. `7` = expires in a week). Omit for a session cookie.\n */\n expires?: Date | number;\n /**\n * `Max-Age` in **seconds** from now. Takes precedence over `expires` in\n * browsers that support it. Use `0` or a negative number to expire immediately.\n */\n maxAge?: number;\n /**\n * The URL path the cookie is scoped to.\n * @default \"/\"\n */\n path?: string;\n /** The domain the cookie is scoped to (e.g. `.example.com`). */\n domain?: string;\n /** When `true`, the cookie is only sent over HTTPS. */\n secure?: boolean;\n /** The `SameSite` policy. `\"none\"` requires `secure: true`. */\n sameSite?: SameSite;\n}\n\n/** `true` when running in an environment that exposes `document`. */\nexport function isDocumentAvailable(): boolean {\n return typeof document !== \"undefined\";\n}\n\n/** Capitalize the first letter (e.g. `\"lax\"` → `\"Lax\"`) for the `SameSite` directive. */\nfunction capitalizeSameSite(value: SameSite): string {\n return value.charAt(0).toUpperCase() + value.slice(1);\n}\n\n/**\n * Default serializer — JSON-encodes any value.\n * @default JSON.stringify\n */\nexport function defaultSerializer<T>(value: T): string {\n return JSON.stringify(value);\n}\n\n/**\n * Default deserializer — tries `JSON.parse`, and **falls back to the raw\n * string** if the value is not valid JSON. Cookies are very often plain,\n * non-JSON strings (set by a server or another library), so this never throws\n * on a legacy/plain value: `\"bar\"` stays `\"bar\"`, while `'{\"a\":1}'` becomes\n * `{ a: 1 }`.\n */\nexport function defaultDeserializer<T>(value: string): T {\n try {\n return JSON.parse(value) as T;\n } catch {\n return value as unknown as T;\n }\n}\n\n/**\n * Parse a raw `document.cookie` string and return the **decoded** value for\n * `key`, or `undefined` when the key is absent. Pure — does not touch\n * `document`, so it is trivially testable.\n *\n * @param cookieString - The raw `document.cookie` value (e.g. `\"a=1; b=2\"`).\n * @param key - The (un-encoded) cookie name to look up.\n */\nexport function parseCookie(\n cookieString: string,\n key: string\n): string | undefined {\n if (!cookieString) return undefined;\n\n const pairs = cookieString.split(\"; \");\n for (const pair of pairs) {\n const eqIndex = pair.indexOf(\"=\");\n const rawName = eqIndex > -1 ? pair.slice(0, eqIndex) : pair;\n\n let name: string;\n try {\n name = decodeURIComponent(rawName);\n } catch {\n name = rawName;\n }\n\n if (name === key) {\n const rawValue = eqIndex > -1 ? pair.slice(eqIndex + 1) : \"\";\n try {\n return decodeURIComponent(rawValue);\n } catch {\n return rawValue;\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Read the decoded value of a cookie from `document.cookie`, guarding for SSR.\n * Returns `undefined` on the server or when the cookie is absent.\n */\nexport function readRawCookie(key: string): string | undefined {\n if (!isDocumentAvailable()) return undefined;\n return parseCookie(document.cookie, key);\n}\n\n/**\n * Build a `document.cookie` assignment string for a cookie, encoding the name\n * and value and appending the standard attributes. Pure and testable.\n *\n * @example\n * buildCookieString(\"theme\", \"dark\", { path: \"/\", maxAge: 3600 })\n * // => \"theme=dark; Path=/; Max-Age=3600\"\n */\nexport function buildCookieString(\n name: string,\n value: string,\n attributes: CookieAttributes = {}\n): string {\n const { expires, maxAge, path = \"/\", domain, secure, sameSite } = attributes;\n\n let str = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;\n\n if (expires !== undefined) {\n const date =\n typeof expires === \"number\"\n ? new Date(Date.now() + expires * 864e5)\n : expires;\n str += `; Expires=${date.toUTCString()}`;\n }\n\n if (maxAge !== undefined) {\n str += `; Max-Age=${maxAge}`;\n }\n\n if (path) {\n str += `; Path=${path}`;\n }\n\n if (domain) {\n str += `; Domain=${domain}`;\n }\n\n if (secure) {\n str += \"; Secure\";\n }\n\n if (sameSite) {\n str += `; SameSite=${capitalizeSameSite(sameSite)}`;\n }\n\n return str;\n}\n\n/**\n * Build the `document.cookie` assignment string that **deletes** a cookie. The\n * value is emptied and the cookie is expired in the past (`Max-Age=-1` plus an\n * epoch `Expires`). The same `path`/`domain` used to write it must be provided,\n * otherwise the browser treats it as a different cookie and deletion is a no-op.\n */\nexport function buildRemovalCookieString(\n name: string,\n attributes: Pick<CookieAttributes, \"path\" | \"domain\"> = {}\n): string {\n const { path = \"/\", domain } = attributes;\n\n let str = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=-1`;\n\n if (path) {\n str += `; Path=${path}`;\n }\n\n if (domain) {\n str += `; Domain=${domain}`;\n }\n\n return str;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// src/useCookie.ts
|
|
2
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
3
|
+
|
|
4
|
+
// src/store.ts
|
|
5
|
+
var listeners = /* @__PURE__ */ new Map();
|
|
6
|
+
function subscribe(key, listener) {
|
|
7
|
+
if (!listeners.has(key)) {
|
|
8
|
+
listeners.set(key, /* @__PURE__ */ new Set());
|
|
9
|
+
}
|
|
10
|
+
const keyListeners = listeners.get(key);
|
|
11
|
+
keyListeners.add(listener);
|
|
12
|
+
return () => {
|
|
13
|
+
keyListeners.delete(listener);
|
|
14
|
+
if (keyListeners.size === 0) {
|
|
15
|
+
listeners.delete(key);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function notifyListeners(key) {
|
|
20
|
+
const keyListeners = listeners.get(key);
|
|
21
|
+
if (keyListeners) {
|
|
22
|
+
keyListeners.forEach((listener) => listener());
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/utils.ts
|
|
27
|
+
function isDocumentAvailable() {
|
|
28
|
+
return typeof document !== "undefined";
|
|
29
|
+
}
|
|
30
|
+
function capitalizeSameSite(value) {
|
|
31
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
32
|
+
}
|
|
33
|
+
function defaultSerializer(value) {
|
|
34
|
+
return JSON.stringify(value);
|
|
35
|
+
}
|
|
36
|
+
function defaultDeserializer(value) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(value);
|
|
39
|
+
} catch {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function parseCookie(cookieString, key) {
|
|
44
|
+
if (!cookieString) return void 0;
|
|
45
|
+
const pairs = cookieString.split("; ");
|
|
46
|
+
for (const pair of pairs) {
|
|
47
|
+
const eqIndex = pair.indexOf("=");
|
|
48
|
+
const rawName = eqIndex > -1 ? pair.slice(0, eqIndex) : pair;
|
|
49
|
+
let name;
|
|
50
|
+
try {
|
|
51
|
+
name = decodeURIComponent(rawName);
|
|
52
|
+
} catch {
|
|
53
|
+
name = rawName;
|
|
54
|
+
}
|
|
55
|
+
if (name === key) {
|
|
56
|
+
const rawValue = eqIndex > -1 ? pair.slice(eqIndex + 1) : "";
|
|
57
|
+
try {
|
|
58
|
+
return decodeURIComponent(rawValue);
|
|
59
|
+
} catch {
|
|
60
|
+
return rawValue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
function readRawCookie(key) {
|
|
67
|
+
if (!isDocumentAvailable()) return void 0;
|
|
68
|
+
return parseCookie(document.cookie, key);
|
|
69
|
+
}
|
|
70
|
+
function buildCookieString(name, value, attributes = {}) {
|
|
71
|
+
const { expires, maxAge, path = "/", domain, secure, sameSite } = attributes;
|
|
72
|
+
let str = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
73
|
+
if (expires !== void 0) {
|
|
74
|
+
const date = typeof expires === "number" ? new Date(Date.now() + expires * 864e5) : expires;
|
|
75
|
+
str += `; Expires=${date.toUTCString()}`;
|
|
76
|
+
}
|
|
77
|
+
if (maxAge !== void 0) {
|
|
78
|
+
str += `; Max-Age=${maxAge}`;
|
|
79
|
+
}
|
|
80
|
+
if (path) {
|
|
81
|
+
str += `; Path=${path}`;
|
|
82
|
+
}
|
|
83
|
+
if (domain) {
|
|
84
|
+
str += `; Domain=${domain}`;
|
|
85
|
+
}
|
|
86
|
+
if (secure) {
|
|
87
|
+
str += "; Secure";
|
|
88
|
+
}
|
|
89
|
+
if (sameSite) {
|
|
90
|
+
str += `; SameSite=${capitalizeSameSite(sameSite)}`;
|
|
91
|
+
}
|
|
92
|
+
return str;
|
|
93
|
+
}
|
|
94
|
+
function buildRemovalCookieString(name, attributes = {}) {
|
|
95
|
+
const { path = "/", domain } = attributes;
|
|
96
|
+
let str = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=-1`;
|
|
97
|
+
if (path) {
|
|
98
|
+
str += `; Path=${path}`;
|
|
99
|
+
}
|
|
100
|
+
if (domain) {
|
|
101
|
+
str += `; Domain=${domain}`;
|
|
102
|
+
}
|
|
103
|
+
return str;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/useCookie.ts
|
|
107
|
+
function resolveInitialValue(initialValue) {
|
|
108
|
+
return typeof initialValue === "function" ? initialValue() : initialValue;
|
|
109
|
+
}
|
|
110
|
+
function useCookie(key, options = {}) {
|
|
111
|
+
const {
|
|
112
|
+
initialValue,
|
|
113
|
+
serializer = defaultSerializer,
|
|
114
|
+
deserializer = defaultDeserializer,
|
|
115
|
+
onError,
|
|
116
|
+
expires,
|
|
117
|
+
maxAge,
|
|
118
|
+
path,
|
|
119
|
+
domain,
|
|
120
|
+
secure,
|
|
121
|
+
sameSite
|
|
122
|
+
} = options;
|
|
123
|
+
const serializerRef = useRef(serializer);
|
|
124
|
+
const deserializerRef = useRef(deserializer);
|
|
125
|
+
const onErrorRef = useRef(onError);
|
|
126
|
+
const initialValueRef = useRef(initialValue);
|
|
127
|
+
serializerRef.current = serializer;
|
|
128
|
+
deserializerRef.current = deserializer;
|
|
129
|
+
onErrorRef.current = onError;
|
|
130
|
+
initialValueRef.current = initialValue;
|
|
131
|
+
const attributesRef = useRef({});
|
|
132
|
+
attributesRef.current = { expires, maxAge, path, domain, secure, sameSite };
|
|
133
|
+
const cacheRef = useRef(null);
|
|
134
|
+
const isClient = isDocumentAvailable();
|
|
135
|
+
const subscribeToStore = useCallback(
|
|
136
|
+
(onStoreChange) => {
|
|
137
|
+
return subscribe(key, onStoreChange);
|
|
138
|
+
},
|
|
139
|
+
[key]
|
|
140
|
+
);
|
|
141
|
+
const getSnapshot = useCallback(() => {
|
|
142
|
+
if (!isClient) {
|
|
143
|
+
return resolveInitialValue(initialValueRef.current);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const rawValue = readRawCookie(key);
|
|
147
|
+
if (cacheRef.current && cacheRef.current.rawValue === rawValue) {
|
|
148
|
+
return cacheRef.current.parsedValue;
|
|
149
|
+
}
|
|
150
|
+
let parsedValue;
|
|
151
|
+
if (rawValue !== void 0) {
|
|
152
|
+
parsedValue = deserializerRef.current(rawValue);
|
|
153
|
+
} else {
|
|
154
|
+
parsedValue = resolveInitialValue(initialValueRef.current);
|
|
155
|
+
}
|
|
156
|
+
cacheRef.current = { rawValue, parsedValue };
|
|
157
|
+
return parsedValue;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
onErrorRef.current?.(error);
|
|
160
|
+
const fallbackValue = resolveInitialValue(initialValueRef.current);
|
|
161
|
+
cacheRef.current = { rawValue: void 0, parsedValue: fallbackValue };
|
|
162
|
+
return fallbackValue;
|
|
163
|
+
}
|
|
164
|
+
}, [key, isClient]);
|
|
165
|
+
const getServerSnapshot = useCallback(() => {
|
|
166
|
+
return resolveInitialValue(initialValueRef.current);
|
|
167
|
+
}, []);
|
|
168
|
+
const storedValue = useSyncExternalStore(
|
|
169
|
+
subscribeToStore,
|
|
170
|
+
getSnapshot,
|
|
171
|
+
getServerSnapshot
|
|
172
|
+
);
|
|
173
|
+
const setValue = useCallback(
|
|
174
|
+
(value) => {
|
|
175
|
+
try {
|
|
176
|
+
if (!isDocumentAvailable()) return;
|
|
177
|
+
const currentValue = (() => {
|
|
178
|
+
try {
|
|
179
|
+
const raw = readRawCookie(key);
|
|
180
|
+
if (raw !== void 0) {
|
|
181
|
+
return deserializerRef.current(raw);
|
|
182
|
+
}
|
|
183
|
+
return resolveInitialValue(initialValueRef.current);
|
|
184
|
+
} catch {
|
|
185
|
+
return resolveInitialValue(initialValueRef.current);
|
|
186
|
+
}
|
|
187
|
+
})();
|
|
188
|
+
const valueToStore = value instanceof Function ? value(currentValue) : value;
|
|
189
|
+
const serialized = serializerRef.current(valueToStore);
|
|
190
|
+
document.cookie = buildCookieString(
|
|
191
|
+
key,
|
|
192
|
+
serialized,
|
|
193
|
+
attributesRef.current
|
|
194
|
+
);
|
|
195
|
+
cacheRef.current = { rawValue: serialized, parsedValue: valueToStore };
|
|
196
|
+
notifyListeners(key);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
onErrorRef.current?.(error);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
[key]
|
|
202
|
+
);
|
|
203
|
+
const removeValue = useCallback(() => {
|
|
204
|
+
try {
|
|
205
|
+
if (!isDocumentAvailable()) return;
|
|
206
|
+
document.cookie = buildRemovalCookieString(key, attributesRef.current);
|
|
207
|
+
const initialVal = resolveInitialValue(initialValueRef.current);
|
|
208
|
+
cacheRef.current = { rawValue: void 0, parsedValue: initialVal };
|
|
209
|
+
notifyListeners(key);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
onErrorRef.current?.(error);
|
|
212
|
+
}
|
|
213
|
+
}, [key]);
|
|
214
|
+
return [storedValue, setValue, removeValue];
|
|
215
|
+
}
|
|
216
|
+
export {
|
|
217
|
+
buildCookieString,
|
|
218
|
+
buildRemovalCookieString,
|
|
219
|
+
parseCookie,
|
|
220
|
+
useCookie
|
|
221
|
+
};
|
|
222
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useCookie.ts","../src/store.ts","../src/utils.ts"],"sourcesContent":["import { useCallback, useRef, useSyncExternalStore } from \"react\";\nimport { subscribe, notifyListeners } from \"./store\";\nimport {\n buildCookieString,\n buildRemovalCookieString,\n defaultDeserializer,\n defaultSerializer,\n isDocumentAvailable,\n readRawCookie,\n type CookieAttributes,\n} from \"./utils\";\n\nexport type { CookieAttributes, SameSite } from \"./utils\";\n\n/**\n * Type for the initial value that can be a value or a function returning a value\n * (lazy initialization).\n */\nexport type InitialValue<T> = T | (() => T);\n\n/**\n * Options for the {@link useCookie} hook. Combines serialization/lifecycle\n * options (aligned with `@usefy/use-local-storage`) with the cookie-specific\n * write attributes from {@link CookieAttributes}.\n */\nexport interface UseCookieOptions<T> extends CookieAttributes {\n /**\n * Initial value used when the cookie is absent (or on the server). Supports a\n * lazy initializer function. This is also the value the state resets to when\n * {@link UseCookieReturn} `remove()` is called.\n */\n initialValue?: InitialValue<T>;\n /**\n * Custom serializer for converting the value to the stored cookie string.\n * @default JSON.stringify\n */\n serializer?: (value: T) => string;\n /**\n * Custom deserializer for parsing the stored cookie string back to a value.\n * The default tries `JSON.parse` and **falls back to the raw string** for\n * non-JSON cookies (so a plain `foo=bar` cookie never throws).\n * @default (value) => { try { return JSON.parse(value) } catch { return value } }\n */\n deserializer?: (value: string) => T;\n /** Callback invoked when a serialization/deserialization error occurs. */\n onError?: (error: Error) => void;\n}\n\n/**\n * Return type for {@link useCookie} — a tuple similar to `useState`.\n */\nexport type UseCookieReturn<T> = readonly [\n /** Current cookie value (`undefined` when the cookie is absent and no `initialValue` was given). */\n T | undefined,\n /** Update the cookie value. Accepts a value or a functional updater `(prev) => next`. */\n React.Dispatch<React.SetStateAction<T | undefined>>,\n /** Delete the cookie and reset state to `initialValue` (or `undefined`). */\n () => void\n];\n\n/**\n * Resolve an initial value, supporting a lazy initializer function.\n */\nfunction resolveInitialValue<T>(\n initialValue: InitialValue<T> | undefined\n): T | undefined {\n return typeof initialValue === \"function\"\n ? (initialValue as () => T)()\n : initialValue;\n}\n\n/**\n * A hook for reading and writing a browser **cookie** as React state — the\n * cookie sibling of `@usefy/use-local-storage` and `@usefy/use-session-storage`.\n * Works like `useState` but the value is persisted in `document.cookie`.\n *\n * Features:\n * - **`useState`-like tuple**: `[value, setValue, remove]`.\n * - **Same-document sync**: multiple `useCookie(key)` instances stay in sync.\n * - **SSR-safe**: guards all `document` access and returns `initialValue` on the\n * server (uses `useSyncExternalStore`, identical to its storage siblings, to\n * avoid hydration mismatches).\n * - **Cookie attributes**: `expires`, `maxAge`, `path`, `domain`, `secure`,\n * `sameSite`.\n * - **Safe (de)serialization**: JSON by default, with a graceful fallback to the\n * raw string for plain (non-JSON) cookies.\n *\n * > **Cross-tab note:** cookies have no `storage` event, so writes in *other*\n * > tabs are not observed without polling. Same-document instances always sync.\n *\n * @template T - The type of the stored value\n * @param key - The cookie name\n * @param options - Serialization, initial value, and cookie write attributes\n * @returns A tuple of `[value, setValue, remove]`\n *\n * @example\n * ```tsx\n * // Basic usage — a string cookie\n * function ThemeToggle() {\n * const [theme, setTheme, removeTheme] = useCookie(\"theme\", {\n * initialValue: \"light\",\n * });\n *\n * return (\n * <div>\n * <p>Current theme: {theme}</p>\n * <button onClick={() => setTheme(\"dark\")}>Dark</button>\n * <button onClick={removeTheme}>Reset</button>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With cookie attributes — persist for a week over HTTPS only\n * const [token, setToken] = useCookie<string>(\"token\", {\n * expires: 7, // days (or a Date)\n * path: \"/\",\n * secure: true,\n * sameSite: \"strict\",\n * });\n * ```\n *\n * @example\n * ```tsx\n * // Object value with a functional update (JSON serialized)\n * interface Prefs { lang: string; compact: boolean }\n * const [prefs, setPrefs] = useCookie<Prefs>(\"prefs\", {\n * initialValue: { lang: \"en\", compact: false },\n * });\n * setPrefs((prev) => ({ ...prev!, compact: true }));\n * ```\n */\nexport function useCookie<T = string>(\n key: string,\n options: UseCookieOptions<T> = {}\n): UseCookieReturn<T> {\n const {\n initialValue,\n serializer = defaultSerializer,\n deserializer = defaultDeserializer,\n onError,\n expires,\n maxAge,\n path,\n domain,\n secure,\n sameSite,\n } = options;\n\n // Store options in refs for stable references and access to the latest values.\n const serializerRef = useRef(serializer);\n const deserializerRef = useRef(deserializer);\n const onErrorRef = useRef(onError);\n const initialValueRef = useRef(initialValue);\n\n serializerRef.current = serializer;\n deserializerRef.current = deserializer;\n onErrorRef.current = onError;\n initialValueRef.current = initialValue;\n\n // Latest cookie write attributes, so setValue/remove stay stable across renders.\n const attributesRef = useRef<CookieAttributes>({});\n attributesRef.current = { expires, maxAge, path, domain, secure, sameSite };\n\n // Cache for getSnapshot so it returns a stable reference when the raw cookie\n // value is unchanged — required by useSyncExternalStore to avoid loops.\n const cacheRef = useRef<{\n rawValue: string | undefined;\n parsedValue: T | undefined;\n } | null>(null);\n\n const isClient = isDocumentAvailable();\n\n // Subscribe to same-document changes via the internal store.\n const subscribeToStore = useCallback(\n (onStoreChange: () => void) => {\n return subscribe(key, onStoreChange);\n },\n [key]\n );\n\n // getSnapshot: read the current value from document.cookie with caching.\n const getSnapshot = useCallback((): T | undefined => {\n if (!isClient) {\n return resolveInitialValue(initialValueRef.current);\n }\n\n try {\n const rawValue = readRawCookie(key);\n\n if (cacheRef.current && cacheRef.current.rawValue === rawValue) {\n return cacheRef.current.parsedValue;\n }\n\n let parsedValue: T | undefined;\n if (rawValue !== undefined) {\n parsedValue = deserializerRef.current(rawValue);\n } else {\n parsedValue = resolveInitialValue(initialValueRef.current);\n }\n\n cacheRef.current = { rawValue, parsedValue };\n return parsedValue;\n } catch (error) {\n onErrorRef.current?.(error as Error);\n const fallbackValue = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: undefined, parsedValue: fallbackValue };\n return fallbackValue;\n }\n }, [key, isClient]);\n\n // getServerSnapshot: return the initial value for SSR.\n const getServerSnapshot = useCallback((): T | undefined => {\n return resolveInitialValue(initialValueRef.current);\n }, []);\n\n const storedValue = useSyncExternalStore(\n subscribeToStore,\n getSnapshot,\n getServerSnapshot\n );\n\n // setValue — stable reference that writes the cookie and notifies listeners.\n const setValue = useCallback<React.Dispatch<React.SetStateAction<T | undefined>>>(\n (value) => {\n try {\n if (!isDocumentAvailable()) return;\n\n // Resolve the current value for functional updates.\n const currentValue = (() => {\n try {\n const raw = readRawCookie(key);\n if (raw !== undefined) {\n return deserializerRef.current(raw);\n }\n return resolveInitialValue(initialValueRef.current);\n } catch {\n return resolveInitialValue(initialValueRef.current);\n }\n })();\n\n const valueToStore =\n value instanceof Function ? value(currentValue) : value;\n\n const serialized = serializerRef.current(valueToStore as T);\n document.cookie = buildCookieString(\n key,\n serialized,\n attributesRef.current\n );\n\n // Invalidate cache so the next getSnapshot reads the fresh value.\n cacheRef.current = { rawValue: serialized, parsedValue: valueToStore };\n\n notifyListeners(key);\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n },\n [key]\n );\n\n // remove — stable reference that deletes the cookie and resets to initial.\n const removeValue = useCallback(() => {\n try {\n if (!isDocumentAvailable()) return;\n\n document.cookie = buildRemovalCookieString(key, attributesRef.current);\n\n const initialVal = resolveInitialValue(initialValueRef.current);\n cacheRef.current = { rawValue: undefined, parsedValue: initialVal };\n\n notifyListeners(key);\n } catch (error) {\n onErrorRef.current?.(error as Error);\n }\n }, [key]);\n\n return [storedValue, setValue, removeValue] as const;\n}\n","/**\n * Internal Store Manager for cookie synchronization.\n * This module manages listeners for same-document synchronization across\n * components using the same cookie key.\n *\n * Unlike `localStorage`, cookies have no native `storage` event, so there is no\n * way to observe cross-tab writes without polling. This store therefore keeps\n * every `useCookie(key)` instance **within the same document** in sync: when one\n * instance writes or removes a cookie it notifies the others. Cross-tab\n * synchronization is intentionally not attempted (see the README).\n *\n * @internal This module is not exported publicly\n */\n\n/** Map of key -> Set of listener callbacks */\nconst listeners = new Map<string, Set<() => void>>();\n\n/**\n * Subscribe a listener to changes for a specific key\n * @param key - The cookie key to subscribe to\n * @param listener - Callback to invoke when the key's value changes\n * @returns Unsubscribe function\n */\nexport function subscribe(key: string, listener: () => void): () => void {\n if (!listeners.has(key)) {\n listeners.set(key, new Set());\n }\n\n const keyListeners = listeners.get(key)!;\n keyListeners.add(listener);\n\n return () => {\n keyListeners.delete(listener);\n\n // Cleanup: remove the key entry if no more listeners\n if (keyListeners.size === 0) {\n listeners.delete(key);\n }\n };\n}\n\n/**\n * Notify all listeners subscribed to a specific key.\n * This is called when setValue or removeValue is invoked to synchronize all\n * components using the same key in the same document.\n *\n * @param key - The cookie key that was updated\n */\nexport function notifyListeners(key: string): void {\n const keyListeners = listeners.get(key);\n if (keyListeners) {\n keyListeners.forEach((listener) => listener());\n }\n}\n\n/**\n * Get the count of listeners for a key (for testing purposes)\n * @internal\n */\nexport function getListenerCount(key: string): number {\n return listeners.get(key)?.size ?? 0;\n}\n\n/**\n * Clear all listeners (for testing purposes)\n * @internal\n */\nexport function clearAllListeners(): void {\n listeners.clear();\n}\n","/**\n * Pure, framework-agnostic helpers for reading and writing `document.cookie`.\n * These are unit-tested in isolation and used by {@link useCookie}.\n */\n\n/**\n * The `SameSite` cookie attribute values.\n *\n * - `\"strict\"` — cookie is sent only for same-site requests.\n * - `\"lax\"` — cookie is sent on same-site requests and top-level navigations.\n * - `\"none\"` — cookie is sent on all requests; **requires `secure: true`**.\n */\nexport type SameSite = \"strict\" | \"lax\" | \"none\";\n\n/**\n * Cookie write attributes, mirroring the standard `Set-Cookie` directives.\n * These control how the cookie is persisted by the browser.\n */\nexport interface CookieAttributes {\n /**\n * Expiry of the cookie. Either an absolute `Date`, or a number of **days**\n * from now (e.g. `7` = expires in a week). Omit for a session cookie.\n */\n expires?: Date | number;\n /**\n * `Max-Age` in **seconds** from now. Takes precedence over `expires` in\n * browsers that support it. Use `0` or a negative number to expire immediately.\n */\n maxAge?: number;\n /**\n * The URL path the cookie is scoped to.\n * @default \"/\"\n */\n path?: string;\n /** The domain the cookie is scoped to (e.g. `.example.com`). */\n domain?: string;\n /** When `true`, the cookie is only sent over HTTPS. */\n secure?: boolean;\n /** The `SameSite` policy. `\"none\"` requires `secure: true`. */\n sameSite?: SameSite;\n}\n\n/** `true` when running in an environment that exposes `document`. */\nexport function isDocumentAvailable(): boolean {\n return typeof document !== \"undefined\";\n}\n\n/** Capitalize the first letter (e.g. `\"lax\"` → `\"Lax\"`) for the `SameSite` directive. */\nfunction capitalizeSameSite(value: SameSite): string {\n return value.charAt(0).toUpperCase() + value.slice(1);\n}\n\n/**\n * Default serializer — JSON-encodes any value.\n * @default JSON.stringify\n */\nexport function defaultSerializer<T>(value: T): string {\n return JSON.stringify(value);\n}\n\n/**\n * Default deserializer — tries `JSON.parse`, and **falls back to the raw\n * string** if the value is not valid JSON. Cookies are very often plain,\n * non-JSON strings (set by a server or another library), so this never throws\n * on a legacy/plain value: `\"bar\"` stays `\"bar\"`, while `'{\"a\":1}'` becomes\n * `{ a: 1 }`.\n */\nexport function defaultDeserializer<T>(value: string): T {\n try {\n return JSON.parse(value) as T;\n } catch {\n return value as unknown as T;\n }\n}\n\n/**\n * Parse a raw `document.cookie` string and return the **decoded** value for\n * `key`, or `undefined` when the key is absent. Pure — does not touch\n * `document`, so it is trivially testable.\n *\n * @param cookieString - The raw `document.cookie` value (e.g. `\"a=1; b=2\"`).\n * @param key - The (un-encoded) cookie name to look up.\n */\nexport function parseCookie(\n cookieString: string,\n key: string\n): string | undefined {\n if (!cookieString) return undefined;\n\n const pairs = cookieString.split(\"; \");\n for (const pair of pairs) {\n const eqIndex = pair.indexOf(\"=\");\n const rawName = eqIndex > -1 ? pair.slice(0, eqIndex) : pair;\n\n let name: string;\n try {\n name = decodeURIComponent(rawName);\n } catch {\n name = rawName;\n }\n\n if (name === key) {\n const rawValue = eqIndex > -1 ? pair.slice(eqIndex + 1) : \"\";\n try {\n return decodeURIComponent(rawValue);\n } catch {\n return rawValue;\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Read the decoded value of a cookie from `document.cookie`, guarding for SSR.\n * Returns `undefined` on the server or when the cookie is absent.\n */\nexport function readRawCookie(key: string): string | undefined {\n if (!isDocumentAvailable()) return undefined;\n return parseCookie(document.cookie, key);\n}\n\n/**\n * Build a `document.cookie` assignment string for a cookie, encoding the name\n * and value and appending the standard attributes. Pure and testable.\n *\n * @example\n * buildCookieString(\"theme\", \"dark\", { path: \"/\", maxAge: 3600 })\n * // => \"theme=dark; Path=/; Max-Age=3600\"\n */\nexport function buildCookieString(\n name: string,\n value: string,\n attributes: CookieAttributes = {}\n): string {\n const { expires, maxAge, path = \"/\", domain, secure, sameSite } = attributes;\n\n let str = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;\n\n if (expires !== undefined) {\n const date =\n typeof expires === \"number\"\n ? new Date(Date.now() + expires * 864e5)\n : expires;\n str += `; Expires=${date.toUTCString()}`;\n }\n\n if (maxAge !== undefined) {\n str += `; Max-Age=${maxAge}`;\n }\n\n if (path) {\n str += `; Path=${path}`;\n }\n\n if (domain) {\n str += `; Domain=${domain}`;\n }\n\n if (secure) {\n str += \"; Secure\";\n }\n\n if (sameSite) {\n str += `; SameSite=${capitalizeSameSite(sameSite)}`;\n }\n\n return str;\n}\n\n/**\n * Build the `document.cookie` assignment string that **deletes** a cookie. The\n * value is emptied and the cookie is expired in the past (`Max-Age=-1` plus an\n * epoch `Expires`). The same `path`/`domain` used to write it must be provided,\n * otherwise the browser treats it as a different cookie and deletion is a no-op.\n */\nexport function buildRemovalCookieString(\n name: string,\n attributes: Pick<CookieAttributes, \"path\" | \"domain\"> = {}\n): string {\n const { path = \"/\", domain } = attributes;\n\n let str = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=-1`;\n\n if (path) {\n str += `; Path=${path}`;\n }\n\n if (domain) {\n str += `; Domain=${domain}`;\n }\n\n return str;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usefy/use-cookie",
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"description": "A React hook for reading and writing browser cookies as React state, SSR-aware",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
24
|
+
"@testing-library/react": "^16.3.1",
|
|
25
|
+
"@testing-library/user-event": "^14.6.1",
|
|
26
|
+
"@types/react": "^19.0.0",
|
|
27
|
+
"jsdom": "^27.3.0",
|
|
28
|
+
"react": "^19.0.0",
|
|
29
|
+
"rimraf": "^6.0.1",
|
|
30
|
+
"tsup": "^8.0.0",
|
|
31
|
+
"typescript": "^5.0.0",
|
|
32
|
+
"vitest": "^4.0.16"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/mirunamu00/usefy.git",
|
|
40
|
+
"directory": "packages/hooks/use-cookie"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react",
|
|
45
|
+
"hooks",
|
|
46
|
+
"cookie",
|
|
47
|
+
"cookies",
|
|
48
|
+
"document.cookie",
|
|
49
|
+
"storage",
|
|
50
|
+
"state",
|
|
51
|
+
"persistence",
|
|
52
|
+
"ssr"
|
|
53
|
+
],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup",
|
|
56
|
+
"dev": "tsup --watch",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:watch": "vitest",
|
|
59
|
+
"typecheck": "tsc --noEmit",
|
|
60
|
+
"clean": "rimraf dist"
|
|
61
|
+
}
|
|
62
|
+
}
|