# Persist state to storage

This page's React example keeps saved favorites across reloads, excludes the draft from storage, and migrates a renamed legacy field. For how persistence reads, writes, and rehydrates state, see [Persisted state lifecycle](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/persisted-state-lifecycle).

## When to use it

Use persistence for state that should remain available after a reload, such as a user's saved favorites. The middleware writes state to storage and rehydrates it when the store initializes. By default it uses JSON-backed `localStorage`.

## Create a persisted React store

Add the middleware to the store hook your component already uses. This example saves favorite names but leaves the editable draft out of storage. It also migrates a version 0 record whose saved names used the `savedItems` field.

Create the hook with [`create`](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/zustand#create) and wrap its state creator with `persist`:

```tsx title="Favorites.tsx"
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

type PersistedFavorites = {
  favorites: string[]
}

type FavoriteStore = PersistedFavorites & {
  draft: string
  setDraft: (draft: string) => void
  addFavorite: () => void
}

function isLegacyFavorites(
  value: unknown,
): value is { savedItems: string[] } {
  return (
    typeof value === 'object' &&
    value !== null &&
    'savedItems' in value &&
    Array.isArray(value.savedItems) &&
    value.savedItems.every((item: unknown) => typeof item === 'string')
  )
}

const useFavoriteStore = create<FavoriteStore>()(
  persist(
    (set, get) => ({
      favorites: [],
      draft: '',
      setDraft: (draft) => set({ draft }),
      addFavorite: () => {
        const favorite = get().draft.trim()
        if (favorite.length > 0) {
          set((state) => ({
            favorites: [...state.favorites, favorite],
            draft: '',
          }))
        }
      },
    }),
    {
      name: 'favorites-storage',
      version: 1,
      partialize: (state) => ({ favorites: state.favorites }),
      migrate: (persistedState, version): PersistedFavorites => {
        if (version === 0 && isLegacyFavorites(persistedState)) {
          return { favorites: persistedState.savedItems }
        }
        return { favorites: [] }
      },
    },
  ),
)

export function Favorites() {
  const favorites = useFavoriteStore((state) => state.favorites)
  const draft = useFavoriteStore((state) => state.draft)
  const setDraft = useFavoriteStore((state) => state.setDraft)
  const addFavorite = useFavoriteStore((state) => state.addFavorite)

  return (
    <main>
      <label>
        Favorite
        <input
          value={draft}
          onChange={(event) => setDraft(event.currentTarget.value)}
        />
      </label>
      <button onClick={addFavorite}>Save favorite</button>
      <ul>
        {favorites.map((favorite) => (
          <li key={favorite}>{favorite}</li>
        ))}
      </ul>
    </main>
  )
}
```

![The page shows the favorite input, save button, and an empty list.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/98aacb9bcfb4131ec706661d291a93bc.png)

Mount the component from your React entry point as shown in [React quick start](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/react-quick-start). The persistence-specific difference is that saved favorites return after a reload while the draft does not.

The page renders an input, a **Save favorite** button, and a list. Enter a name and save it to add that name to the list and `favorites-storage`; reloading restores the list, while the draft starts empty. The persisted object contains only `favorites`, not the draft or the store's action functions.

## Choose the storage key and saved fields

`name` is required and identifies this store's entry in storage. Keep it unique among persisted stores that share the same storage. `partialize` receives the full store state and returns the value to persist; the component still reads the full state in memory. By default, `partialize` returns the state unchanged, so provide it when only some fields belong in storage.

The example uses default JSON-backed `localStorage`. To select a different browser storage such as `sessionStorage`, use [`createJSONStorage`](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/zustand-middleware#createjsonstorage) as the `storage` option, for example `createJSONStorage(() => sessionStorage)`. The helper uses JSON serialization; it does not validate the shape of values read from storage.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `name` | `string` | Required | Selects the unique storage key for this store. |
| `storage` | [`PersistStorage`](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/zustand-middleware#persiststorage)<U> | `createJSONStorage(() => localStorage)` | Chooses the storage used to read and write persisted state. `U` is the shape returned by `partialize`. |
| `partialize` | `(state) => persistedState` | `(state) => state` | Filters the state before it is written. |
| `version` | `number` | `0` | Tags the persisted value with a schema version. |
| `migrate` | `(persistedState, version) => persistedState \| Promise<persistedState>` | Not set | Converts data when the stored version differs from the current version. |

## Migrate a changed schema

Increase `version` when the stored shape changes. In the example, version 1 expects `favorites`, while version 0 stored the same list as `savedItems`. The migration checks the unknown value before reading its field and returns the new persisted shape. If an older version does not have a migration, the middleware does not use that stored value; add a migration for each older shape you intend to retain.

The default JSON storage parses values without runtime shape validation. Validate persisted data in your migration or use a validating custom storage if stored data can be corrupt or untrusted. With asynchronous storage, rehydration occurs after the store's initial render; see [Handle persisted state hydration](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/handle-persisted-state-hydration) if the page depends on the restored value at load time.

## Options to consider

- Use the `storage` option when the default `localStorage` does not fit. The library's `createJSONStorage` helper adapts a [`StateStorage`](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/zustand-middleware#statestorage) such as `sessionStorage` to the JSON-backed storage interface.
- Use `partialize` to exclude transient UI state or actions from the stored value.
- Use `version` with `migrate` when you rename or reshape persisted fields; the migration must return the shape expected by the current store.

For the author's examples of custom URL-backed storage, see the [Hash storage demo](https://stackblitz.com/edit/vitejs-vite-9vg24prg) and [Query storage demo](https://stackblitz.com/edit/vitejs-vite-hyc97ynf). For the store and selector model behind the component, see [How Zustand works](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/how-zustand-works); for delayed restoration from asynchronous storage, see [Handle persisted state hydration](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/handle-persisted-state-hydration).
