Skip to content
D
Documentation

Control persisted hydration

how-to
2 min readUpdated

Use skipHydration when the application must choose when persisted data enters the store, and use onRehydrateStorage to observe completion or report an error.

When to use this

By default, persist hydrates the store during initialization. In an SSR application, or whenever the first render must use only the initial state, set skipHydration: true and start hydration after the appropriate client-side lifecycle point.

Start hydration after the component mounts

Create the store with create and wrap its initializer with persist. The callback returned by onRehydrateStorage runs after hydration: its second argument is defined when hydration fails.

tsx
import { useEffect, useState } from 'react'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

type CounterStore = {
  count: number
  increment: () => void
}

const useCounterStore = create<CounterStore>()(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'counter-storage',
      skipHydration: true,
      onRehydrateStorage: () => {
        console.log('hydration started')

        return (_state, error) => {
          if (error) {
            console.error('hydration failed', error)
          } else {
            console.log('hydration finished')
          }
        }
      },
    },
  ),
)

export function Counter() {
  const count = useCounterStore((state) => state.count)
  const increment = useCounterStore((state) => state.increment)
  const [hydrated, setHydrated] = useState(
    useCounterStore.persist.hasHydrated(),
  )

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

    void useCounterStore.persist.rehydrate()

    return unsubscribe
  }, [])

  return (
    <section>
      <p>{hydrated ? `Saved count: ${count}` : 'Loading saved count…'}</p>
      <button type="button" onClick={increment}>
        Increment
      </button>
    </section>
  )
}
The mounted counter shows the hydration state and the Increment button.

On the first render, the component shows Loading saved count… and the store still contains count: 0. After rehydrate() finishes, the finish listener sets hydrated to true, and the component shows the stored count. The listener returns an unsubscribe function; returning it from the effect removes the listener when the component unmounts.

hasHydrated() is a non-reactive status check. Read it for the initial local value, then use onFinishHydration to update React state when the status changes. rehydrate() returns a promise, so you can also await it from application code when the render lifecycle is not the place to start hydration.

Handle failures

Keep failure handling in the returned callback when you need the error and the completion handling in the same option. The callback receives the hydrated state and no error on success; on failure it receives an error instead. In the sample, success and failure are visible in the browser console, while the component's finish listener controls the rendered loading state.

If your storage is asynchronous, the initial render can still show the default state until hydration completes. Gate dependent UI with the same completion signal rather than assuming persisted values are present during the first render. For nested persisted objects, see Merge persisted state before relying on the default shallow merge.

Was this page helpful?