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.
tsximport { 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),
},
),
)
tsximport { 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.
tsximport { 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.
tsximport { 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.
tsximport { 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 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
| Option | Type | Default | What it does |
|---|---|---|---|
name | string | — | Selects the unique key used in storage. |
storage | storage adapter | createJSONStorage(() => localStorage) | Reads and writes the persisted value through a storage adapter. |
onRehydrateStorage | function or function returning a function | — | Runs custom logic before and after hydration; the returned callback receives the state or an error. |
skipHydration | boolean | undefined | undefined | Prevents 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.
Related
- Persist selected state filters which fields are stored.
- Merge persisted state handles nested objects that need more than the default shallow merge.
- Control persisted hydration covers manual hydration control.
- Migrate persisted state handles versioned stored data.
- See the persist middleware reference for the complete API.
Was this page helpful?