Control when a React store reads persisted state and keep dependent UI behind a loading state until rehydration finishes.
When to use this
Use this pattern when your UI needs persisted values as soon as the page loads, especially with asynchronous storage or server rendering. Asynchronous storage is read after the store's initial render, so rendering from its initial defaults can briefly show the wrong state—for example, treating a persisted signed-in user as signed out.
Defer hydration and gate the UI
- Set
skipHydration: trueonpersist. The store starts from its initial state and waits for an explicitrehydrate()call. - Use
onRehydrateStorageto update a hydration flag after rehydration finishes or encounters an error. Exclude that flag from persisted data so a previous run cannot make the UI appear ready before the current read completes. - In a mounted React component, call
rehydrate()from an effect and render dependent content only when the flag is true.
Build the store hook with create and wrap its state creator with persist. Create bear-store.ts:
tsimport { create } from 'zustand'
import { persist } from 'zustand/middleware'
type BearStore = {
bears: number
hasHydrated: boolean
setHasHydrated: (hasHydrated: boolean) => void
addABear: () => void
}
export const useBearStore = create<BearStore>()(
persist(
(set, get) => ({
bears: 0,
hasHydrated: false,
setHasHydrated: (hasHydrated) => set({ hasHydrated }),
addABear: () => set({ bears: get().bears + 1 }),
}),
{
name: 'hydration-example',
partialize: (state) => ({ bears: state.bears }),
skipHydration: true,
onRehydrateStorage: (state) => () => {
state.setHasHydrated(true)
},
},
),
)
The persisted record contains only bears. The callback closes over the store state passed before the read and marks the store ready when the read finishes, including when storage reports an error.
Create BearCounter.tsx:
tsximport { useEffect } from 'react'
import { useBearStore } from './bear-store'
export function BearCounter() {
const bears = useBearStore((state) => state.bears)
const hasHydrated = useBearStore((state) => state.hasHydrated)
const addABear = useBearStore((state) => state.addABear)
useEffect(() => {
void useBearStore.persist.rehydrate()
}, [])
if (!hasHydrated) {
return <p>Loading saved bears…</p>
}
return (
<main>
<p>Saved bears: {bears}</p>
<button onClick={addABear}>Add a bear</button>
</main>
)
}
Mount BearCounter from your React entry point using the createRoot pattern in React quick start: import this component and render <BearCounter /> instead of CounterApp.
Before the effect starts hydration, the component renders Loading saved bears…. After the persisted value is merged into the store and the callback marks hydration complete, it renders Saved bears: n and an Add a bear button. Clicking the button increments the displayed value and persists the new count. On an error, the callback also releases the loading view, which then displays the current store value.
Options that control hydration
| Option | Type | Default | What it does |
|---|---|---|---|
skipHydration | boolean | undefined | false | Prevents the automatic hydration call during store initialization when true; call rehydrate() when your app is ready. |
onRehydrateStorage | (state: S) => ((state?: S, error?: unknown) => void) | void | undefined | The outer callback runs before the storage read and may return a callback that runs after hydration or an error. |
In the callback type, S is the persisted store's state type. The manual trigger returns Promise<void> | void; this sample uses it to start hydration in a React effect and uses the callback to update renderable state. The UI selects the hydration flag from the store, so React re-renders when the flag changes.
Pitfalls
- With asynchronous storage, the store is not hydrated at the initial render. Gate any UI that depends on restored values instead of treating the initial defaults as loaded data.
- The post-rehydration callback receives an error when storage hydration fails. Decide what your app should show in that case; this example releases the loading state and leaves the current store values visible.
hasHydrated()is a non-reactive getter. Reading it during render does not subscribe the component to later hydration changes; use state such as thehasHydratedfield in this example when rendering must update.
Live demo
Try the Zustand live demo for a running application, then use the sample above to gate a view on persisted state.
Related
- Persisted state lifecycle explains how the middleware reads, merges, and writes state.
- Persist state to storage covers storage setup and partial persistence.
- React quick start shows the basic mounted store-hook pattern.
Was this page helpful?