Choosing a storage
This page compares the three storages a persistent-state definition can use, explains what each one renders on the server and during hydration, and covers the cookie attributes and the limits to plan for.
Choose the storage with the storage option of persistentState():
import { persistentState } from '@nicolopadovan/persistent-state'
export const theme = persistentState('theme', { default: 'system', storage: 'cookie' })export const draft = persistentState('draft', { default: '', storage: 'sessionStorage' })export const filters = persistentState('filters', { default: { status: 'open' } })Without storage, a definition uses localStorage.
Comparison
Section titled “Comparison”| Property | cookie |
localStorage |
sessionStorage |
|---|---|---|---|
| Lifetime | 365 days from each write. Change it with expires; expires: 0 writes a session cookie. |
Until it is removed. expires removes it once past. |
Until the tab is closed. expires removes it once past. |
| Size | Usually about 4 KB per cookie, and browsers limit the number of cookies per domain. | About 5 MiB per origin, depending on the browser. | About 5 MiB per origin, depending on the browser. |
| Readable on the server | Yes: readState, PersistentStateProvider and readCookieState. |
No. The server never reads it. | No. The server never reads it. |
| Sent to the server | With every request to a matching domain and path, including images and scripts. | Never, unless your code sends it. | Never, unless your code sends it. |
| Shared between tabs | Yes, and with subdomains when you set domain. |
Yes, within one origin. | No. Each tab has its own copy. |
| Synced between open tabs | Yes by default, through a BroadcastChannel, and when a hidden tab becomes visible. |
Yes by default, through storage events. |
Never. |
| First render on the server | The stored value when the cookie is listed in a provider; otherwise the default. | The default. | The default. |
| Readable by scripts on the page | Yes. A cookie written by script cannot be HttpOnly. |
Yes. | Yes. |
Set sync: false on a definition to stop it following other tabs. sessionStorage is never synced, whatever sync
says. See Synchronization.
Which storage should I use?
Section titled “Which storage should I use?”- Use a cookie for small state that the first paint depends on or that the server needs: a theme, a layout, whether a sidebar is open, a locale or a currency. It is the only storage that can render without a flash, and it is sent with every request, so keep the value small.
- Use
localStoragefor client-only state that is larger or that the server does not need: filters, drafts, dismissed notices. It costs nothing per request. The value appears in one render after the first one, as the next section describes. - Use
sessionStoragefor state that belongs to one tab and should not leak into others: the step of a wizard in progress or an unsaved draft.
Changing the storage of a key does not move what is already stored. A definition reads only its own storage, so
visitors start from the default; read the old value with a definition of the old storage and set() it on the new one
if you need to keep it.
Server rendering and hydration
Section titled “Server rendering and hydration”React compares the HTML the server rendered with the first render on the client. The snapshot that useStoredState()
gives React must therefore be equal on the server and during hydration, so a definition renders as follows:
| Storage | Server render | Hydration | After hydration |
|---|---|---|---|
cookie, listed in a provider |
The value decoded from the provider’s raw cookie. | The same raw cookie, decoded the same way. | document.cookie, normally the same value: no flash. |
cookie, no provider value |
The default. | The default. | The stored value, after one render with the default. |
localStorage, sessionStorage |
The default. | The default. | The stored value, after one render with the default. |
The server never reads Web Storage, and the client never reads storage while hydrating. Use storage: 'cookie' for state
that the first paint depends on, such as a theme or the open state of a sidebar, and list it in
PersistentStateProvider. The Next.js guide shows how. In other React server-rendering frameworks,
render CookieValuesProvider with the request’s cookies.
Cookies
Section titled “Cookies”A cookie definition writes its cookie with document.cookie. These options control its attributes, and each one is
applied on every write:
| Option | Default | Meaning |
|---|---|---|
expires |
365 |
Days from each write, or a Date. 0 writes a session cookie. |
path |
'/' |
The cookie’s Path. The browser sends the cookie only for requests under it. |
domain |
None | The cookie’s Domain. Without it, the cookie belongs to the current host and not its subdomains. |
secure |
true on https: pages |
Whether the cookie carries Secure. |
sameSite |
'lax' |
The cookie’s SameSite attribute: 'strict', 'lax' or 'none'. |
import { persistentState } from '@nicolopadovan/persistent-state'
export const locale = persistentState('locale', { default: 'en', storage: 'cookie', expires: 180, domain: 'example.com', sameSite: 'lax',})The cookie attributes are a type error for a definition whose storage is not 'cookie'. Keep the default path for
state that the server reads: with another path, only requests under that path carry the cookie, and the server sees the
default everywhere else.
A cookie is sent with every matching request, so a large value makes every request larger, and servers and proxies limit
the total size of the request headers. After each write, persistent-state reads document.cookie back. A cookie that
the browser refused or that exceeded its limit does not read back: the write is reported to onError as write-failed,
isPersistent becomes false, and the value stays in memory. A later successful write sets isPersistent back to
true.
Cookies written by older versions, duplicates and reset()
Reads accept a cookie under any name an older version may have written it with: the canonical name, the raw key, and its
single and double URI-encoded forms. The first name that is present wins. reset() expires the cookie under every one
of those names, with the definition’s path and domain.
reset() therefore cannot reach a cookie of the same name that was set on a different path or on a different domain.
That cookie stays, and a later read can find it and bring an old value back.
Two cookies of one name can also coexist when they were set with different paths or domains. The browser lists both in
document.cookie, and persistent-state uses the first one on the client. A Next.js server uses the last one, because
its request cookies keep the last value of a repeated name. The server render and the client can then show different
values. Remove the stray cookie, for example by expiring it with its original path and domain.
Web Storage
Section titled “Web Storage”localStorage and sessionStorage hold about 5 MiB per origin, depending on the browser, and never leave the browser on
their own.
- When a write throws because the storage is full, the error is reported to
onErroraswrite-failed,isPersistentbecomesfalseand the value stays in memory. A full storage still lets stored values be read and removed. - When the storage is unavailable, for example because the browser blocks it, persistent-state reports
unavailableonce, keeps the value in memory for the page, and setsisPersistenttofalse. A storage failure is reported toonErrorand never thrown. - An
expiresthat has passed is checked when the item is read, not by a timer. See Expiry. - Browsers may remove storage on their own. Safari, for example, deletes data created by script for an origin that the visitor has not interacted with for seven days of browser use, when cross-site tracking prevention is on; cookies set by the server are exempt. See the MDN page on storage quotas and eviction criteria. Treat stored state as a preference you can recreate, not as the only copy of data.
Synchronization
Section titled “Synchronization”With sync on, which is the default, definitions follow changes made in other tabs:
localStorage:storageevents for the stored name run the read again, including the one a tab sends when it callsclear().cookie: after a successful cookie write or removal, the writing tab posts a message on aBroadcastChannelnamedpersistent-state. The message carries only the definition’s identity, and the receiving tab readsdocument.cookieitself. Cookie definitions also read again when a hidden tab becomes visible, which picks up cookies that the server set in the meantime.sessionStorage: never synced.- A value that lives only in memory, because storage is unavailable or
consentis not given, is never synced.
Inside one tab, every hook and definition with the same storage and full key shares one record and updates together,
regardless of sync.
Privacy
Section titled “Privacy”Stored values are not encrypted or signed. The visitor can read and change them, and so can any script that runs on the page, in all three storages. Never store a secret or a session credential in persistent-state, and validate what you read with a schema before you trust it.
Whether a stored value needs the visitor’s consent depends on its purpose and on the law that applies to you.
persistent-state does not decide that, and nothing in these docs states that a setup complies with a law. The consent
option stores a value only while you allow it: see Consent.