Keep a value between visits, follow changes, and read cookies on the server.
Use the storage helpers to save a value and read it back later. They support localStorage, sessionStorage, and cookies, and let you listen for changes.
For a user preference, such as a theme or sidebar state, pass a storage to createPreference.
Choose a Storage
Choose by how long the value should last and whether the server needs to read it:
| Storage | Lasts | Shared With | Server Reads It |
|---|---|---|---|
createLocalStorage | Until removed | Other tabs on the site | No |
createSessionStorage | While the tab is open | This tab | No |
createCookieStorage | One year, or maxAge seconds | Requests and other tabs | Yes |
A note you want to keep between visits can use localStorage. A temporary draft can use sessionStorage. A preference that changes server-rendered content can use a cookie.
Keep a Value
These helpers store strings under a name, which becomes the localStorage or sessionStorage key, or the cookie's name:
import { createLocalStorage } from "@ui-sdk/core"
const note = createLocalStorage("note")
note.set("Buy milk")
note.get()
note.remove()
note.get()The first get() returns "Buy milk". After remove(), it returns null.
import * as React from "react"
import {
createCookieStorage,
createLocalStorage,
createSessionStorage,
type ValueStorage,
} from "@ui-sdk/core"
import {
Field,
FieldControl,
FieldDescription,
FieldLabel,
} from "@ui-sdk/react/field"
import {
SegmentedControl,
SegmentedControlIndicator,
SegmentedControlItem,
} from "@ui-sdk/react/segmented-control"
const storages: { label: string; storage: ValueStorage; lasts: string }[] = [
{
label: "localStorage",
storage: createLocalStorage("ui-sdk-note"),
lasts: "Stays until cleared, in every tab.",
},
{
label: "sessionStorage",
storage: createSessionStorage("ui-sdk-note"),
lasts: "Stays while this tab is open.",
},
{
label: "Cookie",
storage: createCookieStorage("ui-sdk-note"),
lasts: "Stays a year, and the server reads it too.",
},
]
const noServerValue = () => null
function Example() {
const [picked, setPicked] = React.useState("localStorage")
const { storage, lasts } = storages.find(({ label }) => label === picked)!
const note = React.useSyncExternalStore(
storage.subscribe,
storage.get,
noServerValue
)
const [available, setAvailable] = React.useState(true)
React.useEffect(() => setAvailable(storage.available()), [storage])
return (
<div data-slot="stack">
<div data-slot="controls">
<SegmentedControl
aria-label="Keep the note in"
value={picked}
onValueChange={(next) => next && setPicked(next)}
>
<SegmentedControlIndicator />
{storages.map(({ label }) => (
<SegmentedControlItem key={label} value={label}>
{label}
</SegmentedControlItem>
))}
</SegmentedControl>
</div>
<div data-slot="stack">
<Field disabled={!available}>
<FieldLabel>Note</FieldLabel>
<FieldControl
value={note ?? ""}
onChange={(event) => storage.set(event.target.value)}
placeholder={
available ? "Type, then reload the page" : "Unavailable here"
}
/>
<FieldDescription>
{available ? lasts : "This browser blocks it here."}
</FieldDescription>
</Field>
<button
type="button"
onClick={() => storage.remove()}
disabled={note === null}
>
Clear Note
</button>
</div>
</div>
)
}
export { Example }When Storage Is Unavailable
A browser can block storage or run out of space. If a read fails, get() returns null. For localStorage and sessionStorage, set and remove return true when the write succeeds, or false when it fails.
Use available() for an initial check. A later write can still fail, so check its return value too:
if (!note.available()) {
console.log("Saving is unavailable here.")
}
if (!note.set("Buy milk")) {
console.log("The note wasn't saved.")
}A full storage may still let you read or remove an existing value.
For cookies, true means no error was reported. It doesn't confirm that the cookie was saved or removed.
On the server, localStorage and sessionStorage are unavailable. To read a saved value there, use a cookie.
Follow Changes
Read the starting value with get, then subscribe to changes. subscribe doesn't call your listener with the initial value:
function logNote(value: string | null) {
console.log(value ?? "")
}
logNote(note.get())
const stop = note.subscribe(logNote)Writes through a storage with the same kind and ID notify listeners on the page, including writes through note itself. LocalStorage also follows changes in other tabs. Cookies follow other tabs in browsers that support cookieStore.
Call the returned function when you no longer need the subscription, such as when the component unmounts:
stop()Read a Cookie on the Server
Pass the request's Cookie header to read a saved value during server rendering:
import { createCookieStorage } from "@ui-sdk/core"
function readDensity(request: Request) {
const density = createCookieStorage("density", {
cookie: request.headers.get("cookie"),
})
return density.get()
}Create the storage inside your request handler, as above, so each visitor's cookie stays separate. In the browser, it reads document.cookie.
On the server, available() returns true when you supply a Cookie header, but set and remove return false. The helper reads the request's cookies; it doesn't set response headers. Its readsOnServer flag lets helpers such as createPreference use the saved value in their initial state.
By default, cookies last one year, use path / and SameSite=Lax, and use Secure on HTTPS pages. You can change these with maxAge, path, domain, sameSite, and secure.
Show a Value Before Hydration
The server can't read localStorage or sessionStorage. For styles that depend on a saved value, script generates JavaScript that puts it on a data-* attribute before hydration:
import { createLocalStorage } from "@ui-sdk/core"
const densityStorage = createLocalStorage("density")
const script = densityStorage.script({
attribute: "data-density",
values: ["comfortable", "compact"],
})Render the element with its default attribute, such as data-density="comfortable". Render script as inline JavaScript immediately after that element. It reads the saved value and updates the preceding element's attribute while the page loads.
Only values listed in values are written. Missing or invalid saved values leave the attribute as the server rendered it. Attribute names must start with data-.
The script updates the attribute. Read the storage when the component mounts and subscribe to keep its state current. When you render the script in your framework, account for the attribute changing before hydration.
Scripts with the same storage kind and ID share one initial read. A cookie storage's script returns undefined, because the server can render the cookie's value itself.
For user preferences, apply them with Preferences to render one script and keep their attributes up to date.
API Reference
idstring- The ID the value is kept under: the localStorage or sessionStorage key, or the cookie's name.
- Example: Keep a Value
readsOnServerboolean- Whether the server reads the value too, so it renders right from the first render. True for cookies.
- Example: Read a Cookie on the Server
script(options: StorageScriptOptions) => string- The inline script a component renders right after its root, which puts the saved value on the root before hydration, so CSS shows it on the first paint. Undefined for a cookie, since the server renders its value.
- Example: Show a Value Before Hydration
available() => boolean- Whether storage appears available. A later write can still fail. On the server, a cookie with a request header is available for reading only.
- Example: When Storage Is Unavailable
get() => string | null- The value kept under the ID, or null when nothing is, or the storage is unavailable.
- Example: Keep a Value
set(value: string) => boolean- Saves a value under the ID. Returns false when writing is unavailable or the browser reports a write failure. For cookies, true means the assignment didn't throw; it doesn't confirm the cookie was saved.
- Example: Keep a Value
remove() => boolean- Removes the value. Returns false when writing is unavailable or the browser reports a write failure. For cookies, true means the assignment didn't throw; it doesn't confirm the cookie was removed.
- Example: Keep a Value
subscribe(listener: (value: string | null) => void) => () => void- Hears the value change, through another storage with the same ID on the page or in another tab. Returns the function that stops it.
- Example: Follow Changes
cookie?string | null- The request's
Cookieheader, so the server reads the same value the browser will. - Example: Read a Cookie on the Server
domain?string- The domain that sends the cookie. Defaults to the page's host.
- Example: Read a Cookie on the Server
sameSite?"lax" | "none" | "strict"- Whether other sites' requests send the cookie.
- Example: Read a Cookie on the Server
secure?boolean- Whether only HTTPS requests send the cookie. Defaults to true on HTTPS pages.
- Example: Read a Cookie on the Server
attribute`data-${string}`- The attribute that shows the value, which has to start with
data-, like"data-color-scheme". - Example: Show a Value Before Hydration
valuesreadonly string[]- The values it may show. Anything else saved under the ID leaves the root as the server rendered it.
- Example: Show a Value Before Hydration