Skip to content
D
Documentation

Handle persisted state hydration

how-to
3 min readUpdated

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

  1. Set skipHydration: true on persist. The store starts from its initial state and waits for an explicit rehydrate() call.
  2. Use onRehydrateStorage to 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.
  3. 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:

ts
import { 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:

tsx
import { 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

OptionTypeDefaultWhat it does
skipHydrationboolean | undefinedfalsePrevents the automatic hydration call during store initialization when true; call rehydrate() when your app is ready.
onRehydrateStorage(state: S) => ((state?: S, error?: unknown) => void) | voidundefinedThe 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 the hasHydrated field 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.

Was this page helpful?

Handle persisted state hydration — zustand · GPT-6 Luna