Skip to content
ultrastorageultrastorage
Esc
navigateopen⌘Jpreview
On this page

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 './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, 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 string Key relative to the instance’s 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 both writes a non-expiring entry.

Read options

Option Behavior
defaultValue Display-only fallback when the read returns null; never automatically written. An omitted or undefined default uses null.
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 the existing ttl or expiresAt options; omitted expiration options write a non-expiring entry. 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 key stay the same. 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.

Was this page helpful?