Skip to content
D
Documentation

Persist state to storage

how-to
3 min readUpdated

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.

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 and wrap its state creator with persist:

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.

Mount the component from your React entry point as shown in 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 as the storage option, for example createJSONStorage(() => sessionStorage). The helper uses JSON serialization; it does not validate the shape of values read from storage.

OptionTypeDefaultWhat it does
namestringRequiredSelects the unique storage key for this store.
storagePersistStoragecreateJSONStorage(() => localStorage)Chooses the storage used to read and write persisted state. U is the shape returned by partialize.
partialize(state) => persistedState(state) => stateFilters the state before it is written.
versionnumber0Tags the persisted value with a schema version.
migrate(persistedState, version) => persistedState | Promise<persistedState>Not setConverts 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 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 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 and Query storage demo. For the store and selector model behind the component, see How Zustand works; for delayed restoration from asynchronous storage, see Handle persisted state hydration.

Was this page helpful?

Persist state to storage — zustand · GPT-6 Luna