Skip to content
react-global-state-hooks

Persistence

Save a store to localStorage, restore it safely, migrate old data, save only part of the state, and avoid hydration mismatches.

GuideTypeScriptTry an example

The problem this solves

Preferences, drafts and sessions should survive a reload. The web package can persist a global store with one option: localStorage: { key }.

What is stored

The store writes its initial state when nothing is stored yet, and rewrites on every change. The stored value is a small JSON envelope { "s": <state>, "v": <version> }. Treat the format as an implementation detail and read the store through its API. Values such as Date, Map and Set are serialised and restored for you.

Validate and select

validator runs after every restore. Return the state to use. Return initial to discard bad data. Return nothing to accept the restored value as is. Throwing counts as bad data too, which is what makes a schema library a one-line validator. Validate the state, not the fragment: restored holds only what the selector saved, so merge it over initial first, validator: ({ restored, initial }) => schema.parse({ ...initial, ...restored }).

preferences.ts
import { createGlobalState } from 'react-global-state-hooks';
import { z } from 'zod';

// The shape of the whole state, saved fields and the rest alike.
const schema = z.object({
  theme: z.enum(['light', 'dark']),
  language: z.string(),
  sessionOnly: z.string(),
});

export type Preferences = z.infer<typeof schema>;

const initialPreferences: Preferences = { theme: 'light', language: 'en', sessionOnly: 'not saved' };

export const usePreferences = createGlobalState(initialPreferences, {
  localStorage: {
    key: 'docs:preferences',

    // Save only part of the state.
    selector: (state) => ({ theme: state.theme, language: state.language }),

    // Storage only carries the saved fields, so validate the state they produce. A parse error is
    // caught by the store, which then keeps the initial state.
    validator: ({ restored, initial }) => schema.parse({ ...initial, ...(restored as Partial<Preferences>) }),
  },
});

Migrate old data

Set versioning.version and a migrator. When the stored version differs, the migrator receives the old value as legacy and must return the new state. The validator then runs on the result.

settings.ts
import { createGlobalState } from 'react-global-state-hooks';

// Version 1 stored { theme }. Version 2 added `compact`.
export const useSettings = createGlobalState(() => ({ theme: 'light', compact: false }), {
  localStorage: {
    key: 'docs:settings',
    versioning: {
      version: 2,
      // Called when the stored version differs from `version`.
      migrator: ({ legacy, initial }) => {
        const old = legacy as { theme?: string } | null;

        return { ...initial, theme: old?.theme ?? initial.theme };
      },
    },
  },
});

Options at a glance

OptionPurpose
keyThe localStorage key. Required.
validator({ restored, initial })Check or transform restored data. A throw discards it.
selector(state)Choose what to save.
versioning: { version, migrator }Upgrade data saved by older code.
onError(error, tools)Handle storage errors. Without it, errors are logged with console.error.
adapter: { getItem, setItem }Take over reading and writing. This disables versioning, migration and selector.

What to watch for

  • Persistence is skipped when localStorage is unavailable, for example during server rendering.
  • A stored value that is not a valid envelope is reported and the store falls back to its initial state.
  • In tests, reset() with no arguments clears the stored value first. Combine it with a function initializer to get the original state back.
  • Storage can throw (private windows, quota). Provide onError if you need to react.
Documentation
↑↓ Navigate ↵ Open Esc CloseRGSH docs
Get started

Shared state. Precise subscriptions.
Built-in DevTools.