Skip to content
D
Documentation

Persist store data

how-to
2 min readUpdated

Use persist when a store must retain its state across page reloads. Give the store a unique name, choose a storage adapter, and account for the time at which hydration completes.

Create a persisted React store

Wrap the state creator passed to create with persist. This example stores the bear count in sessionStorage; the mounted component displays the count and updates it when you click the button.

tsx
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'

type BearStore = {
  bears: number
  addBear: () => void
}

export const useBearStore = create<BearStore>()(
  persist(
    (set) => ({
      bears: 0,
      addBear: () => set((state) => ({ bears: state.bears + 1 })),
    }),
    {
      name: 'bear-counter',
      storage: createJSONStorage(() => sessionStorage),
    },
  ),
)
tsx
import { useBearStore } from './bear-store'

export function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  const addBear = useBearStore((state) => state.addBear)

  return (
    <button type="button" onClick={addBear}>
      Bears: {bears}
    </button>
  )
}

name is the storage key and is the only required persistence option, so use a different value for each store. If you omit storage, persistence uses JSON storage backed by localStorage. createJSONStorage converts a storage engine with getItem, setItem, and removeItem methods into the adapter expected by persist.

Supply a custom storage engine

Implement StateStorage when the storage engine needs its own key handling or API. The adapter below prefixes every key while retaining the browser's synchronous storage behaviour. The storage getter is a function, so the adapter can obtain a browser storage object only when the store is created.

tsx
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'
import type { StateStorage } from 'zustand/middleware'

type BearStore = {
  bears: number
  addBear: () => void
}

const prefixedStorage: StateStorage = {
  getItem: (name) => sessionStorage.getItem(`demo:${name}`),
  setItem: (name, value) => sessionStorage.setItem(`demo:${name}`, value),
  removeItem: (name) => sessionStorage.removeItem(`demo:${name}`),
}

export const useBearStore = create<BearStore>()(
  persist(
    (set) => ({
      bears: 0,
      addBear: () => set((state) => ({ bears: state.bears + 1 })),
    }),
    {
      name: 'bear-counter',
      storage: createJSONStorage(() => prefixedStorage),
    },
  ),
)

The adapter receives JSON strings from createJSONStorage; do not parse or stringify them again in these methods. The JSON helper uses JSON.parse and JSON.stringify without runtime shape validation, so validate untrusted or stale data in a custom adapter before treating it as store state.

Handle asynchronous hydration

localStorage and sessionStorage are synchronous. An asynchronous adapter returns a promise from getItem, so the first render can show the store's defaults while persisted data is loading. Wait for hydration before presenting UI that depends on the persisted value.

tsx
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'
import type { StateStorage } from 'zustand/middleware'

type BearStore = {
  bears: number
  addBear: () => void
}

const asyncStorage: StateStorage<Promise<void>> = {
  getItem: async (name) => {
    await new Promise<void>((resolve) => window.setTimeout(resolve, 100))
    return sessionStorage.getItem(name)
  },
  setItem: async (name, value) => {
    sessionStorage.setItem(name, value)
  },
  removeItem: async (name) => {
    sessionStorage.removeItem(name)
  },
}

export const useAsyncBearStore = create<BearStore>()(
  persist(
    (set) => ({
      bears: 0,
      addBear: () => set((state) => ({ bears: state.bears + 1 })),
    }),
    {
      name: 'async-bear-counter',
      storage: createJSONStorage(() => asyncStorage),
    },
  ),
)

Register an onFinishHydration listener in the component and remove it when the component unmounts. useStore reads the store's state; the listener changes the component's local hydrated flag when the persisted state has been merged.

tsx
import { useEffect, useState } from 'react'
import { useStore } from 'zustand'

import { useAsyncBearStore } from './async-bear-store'

export function AsyncBearCounter() {
  const bears = useStore(useAsyncBearStore, (state) => state.bears)
  const addBear = useStore(useAsyncBearStore, (state) => state.addBear)
  const [hydrated, setHydrated] = useState(() =>
    useAsyncBearStore.persist.hasHydrated(),
  )

  useEffect(() => {
    const unsubscribe = useAsyncBearStore.persist.onFinishHydration(() => {
      setHydrated(true)
    })

    return unsubscribe
  }, [])

  if (!hydrated) {
    return <p>Loading saved bears…</p>
  }

  return (
    <button type="button" onClick={addBear}>
      Bears: {bears}
    </button>
  )
}
The mounted counter displays the persisted bear count after hydration.

The loading message can appear on the initial render with asynchronous storage. After hydration finishes, the component renders the persisted count. The onFinishHydration method returns the unsubscribe function, so the listener does not outlive the component.

Options that matter here

OptionTypeDefaultWhat it does
namestring—Selects the unique key used in storage.
storagestorage adaptercreateJSONStorage(() => localStorage)Reads and writes the persisted value through a storage adapter.
onRehydrateStoragefunction or function returning a function—Runs custom logic before and after hydration; the returned callback receives the state or an error.
skipHydrationboolean | undefinedundefinedPrevents automatic initial hydration, leaving the first call to rehydrate() to the application.

Use skipHydration when the application controls when hydration begins, such as an SSR setup. Otherwise, let persist hydrate on initialization and gate asynchronous-storage UI as shown above.

Was this page helpful?