React API
React hook signatures, read options, return values, and exported types.
Import the React adapter from ultrastorage/react. React 18 and 19 are supported. See the React guide for setup, examples, snapshots, and server rendering.
createStorageHook(storage: UltraStorage): StorageHook
Binds a storage instance and returns a hook that accepts (key, options?).
The bound hook uses that instance’s configured Storage object, prefix, separator, and serializer,
with the same read options and return value as useStorage.
Create the bound hook outside components; no provider is required.
Example
Create a shared instance and export its bound hook:
import { createStorage } from 'ultrastorage';
import { createStorageHook } from 'ultrastorage/react';
export const appStorage = createStorage({ prefix: 'app' });
export const useAppStorage = createStorageHook(appStorage);
Components can read and update the same key without passing the instance. This example uses Zod for the schema option, a display-only defaultValue, and ttl on a write:
'use client';
import { z } from 'zod';
import { useAppStorage } from './lib/app-storage';
const ThemeSchema = z.enum(['light', 'dark']);
export function ThemePicker() {
const [theme, setTheme, removeTheme] = useAppStorage('theme', {
schema: ThemeSchema,
defaultValue: 'light',
});
return (
<>
<button onClick={() => setTheme((previous) => (previous === 'light' ? 'dark' : 'light'))}>
Theme: {theme}
</button>
<button onClick={() => setTheme('dark', { ttl: 60 * 60 * 1000 })}>
Use dark theme for one hour
</button>
<button onClick={removeTheme}>Reset theme</button>
</>
);
}
export function CurrentTheme() {
const [theme] = useAppStorage('theme', {
schema: ThemeSchema,
defaultValue: 'light',
});
return <p>Current theme: {theme}</p>;
}
Mount both components anywhere in the app. Toggling the theme updates both, and resetting it removes
the stored entry so both display 'light'. They also respond to
appStorage.setItem('theme', 'dark') outside React.
Both consumers use the same schema and default so they agree on invalid or missing data. The schema validates stored values on read; the default is never automatically written.
The one-hour button passes ttl to the setter. Expiration is lazy by default, so elapsed time alone does not
trigger a render; see snapshots and expiration.
useStorage(storage, key, options?)
Subscribes to a key and returns its current value with callbacks to write or remove it. Calling this hook directly is equivalent to calling a bound hook.
Use useStorage() when choosing the storage instance is part of the component’s job:
- The instance comes from props or context. A reusable component can work with whichever store its caller supplies, including a memory-backed instance in tests.
- The instance can change. For example, a workspace picker can select among existing storage instances with different prefixes. Passing the selected instance to
useStorage()switches the subscription. - You only need a one-off call. A component can pass an existing instance directly without defining and exporting a named hook.
Keep the instance stable between renders unless you intend to switch stores. When several components
always use the same shared instance, prefer createStorageHook() to avoid passing it repeatedly.
Both APIs support changing keys, so a dynamic key alone does not require useStorage().
| Parameter | Type | Description |
|---|---|---|
storage |
UltraStorage |
Factory-created instance from ultrastorage or ultrastorage/core. |
key |
StorageKey |
String or ordered array of string segments relative to the namespace. |
options |
UseStorageOptions<T> |
Optional display fallback and synchronous read schema. |
Here, DailyCounter receives its instance through props, so its parent chooses where the count is
stored. The direct hook accepts the same read options as a bound hook.
Write options go on the setter; this example uses an absolute expiresAt instead of a ttl:
import type { UltraStorage } from 'ultrastorage';
import { useStorage } from 'ultrastorage/react';
import { z } from 'zod';
const CountSchema = z.number().int().nonnegative();
export function DailyCounter({ storage }: { storage: UltraStorage }) {
const [count, setCount, removeCount] = useStorage(storage, 'count', {
schema: CountSchema,
defaultValue: 0,
});
function increment() {
const midnight = new Date();
midnight.setHours(24, 0, 0, 0);
setCount((previous) => previous + 1, { expiresAt: midnight });
}
return (
<>
<button onClick={increment}>Count: {count}</button>
<button onClick={removeCount}>Reset count</button>
</>
);
}
count and the updater’s previous value are inferred as number from the schema and default. Each
increment sets expiry to the next local midnight.
Expiration is lazy here too; the counter does not automatically rerender at midnight. Use either
ttl or expiresAt on a write, never both. Omitting expiration options preserves the current
unexpired deadline; expiresAt: null removes expiration.
Read options
| Option | Behavior |
|---|---|
defaultValue |
Display-only fallback when the read returns null; never automatically written. An omitted or undefined default uses null. |
reactiveExpiration |
Optional boolean, default false. Update at expiration without deleting data; timers may run late. |
schema |
Synchronous Standard Schema validation and transformation, matching getItem(). Infer the value type from its output. |
Missing, expired, foreign, unparseable, invalid, and stored-null values use the fallback. Valid stored
undefined remains undefined. A non-null default removes null from the inferred value and updater
input types.
Defaults should match the schema output type; defaults themselves are not validated. Setter input uses that same output type, and writes are not schema-validated.
Return value
useStorage() and the bound hook return a readonly [value, setValue, removeValue] tuple.
setValue(valueOrUpdater, storageOptions?) accepts ttl or expiresAt, never both.
Omitted expiration options preserve the current unexpired deadline. Pass expiresAt: null
to remove expiration. Missing or expired entries are written without expiration by default.
Functional updates read the latest persisted value, apply schema validation and the fallback, then
write the result. They are synchronous but are not atomic across tabs. removeValue() removes the
entry and restores the displayed default.
Instance and subscription behavior
Setter and remover identities remain stable while the storage instance and structural key stay the same. Equivalent array keys do not switch subscriptions when recreated during render.
Changes to hook options apply after commit; switching instances or keys switches subscriptions.
Use factory-created ultrastorage instances from ultrastorage or ultrastorage/core; structural
mocks or independently loaded package copies are not supported by the private snapshot bridge. For
tests, wrap createMemoryStorage() with createStorage().
Exported types
The adapter exports UseStorageOptions, UseStorageResult, StorageSetter, and StorageHook types. It does not add a provider, public snapshot API, selector, or useStorageValue hook.