Use version and migrate when a breaking state-shape change makes the data already in storage incompatible with the store in your code. The migration runs during rehydration, returns the current state shape, and lets persist write the migrated value back to storage.
When to use this
Add a new version when you rename, remove, or otherwise change a persisted field. Keep the same name so existing data is found, and make migrate handle the stored versions your application still supports.
Migrate a renamed field
-
Install Zustand in your application:
bashnpm install zustand -
Set the storage item to the old shape and create the store with a new version. This complete React example renames
oldLayouttolayout:tsximport { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { create } from 'zustand' import { persist } from 'zustand/middleware' type Layout = 'compact' | 'comfortable' type SettingsStore = { theme: 'light' | 'dark' layout: Layout } type LegacySettings = { theme?: 'light' | 'dark' oldLayout?: Layout } function isLegacySettings(value: unknown): value is LegacySettings { return typeof value === 'object' && value !== null } localStorage.setItem( 'settings', JSON.stringify({ state: { theme: 'dark', oldLayout: 'compact' }, version: 0, }), ) const useSettingsStore = create<SettingsStore>()( persist<SettingsStore>( () => ({ theme: 'light', layout: 'comfortable', }), { name: 'settings', version: 1, migrate: (persistedState, version) => { if (version === 0 && isLegacySettings(persistedState)) { return { theme: persistedState.theme ?? 'light', layout: persistedState.oldLayout ?? 'comfortable', } } return { theme: 'light', layout: 'comfortable', } }, }, ), ) function Settings() { const theme = useSettingsStore((state) => state.theme) const layout = useSettingsStore((state) => state.layout) return <p>{theme} theme, {layout} layout</p> } const container = document.getElementById('root')! createRoot(container).render( <StrictMode> <Settings /> </StrictMode>, )The mounted component shows the migrated dark theme and compact layout. The mounted component reads the migrated state and displays
dark theme, compact layout. During hydration, Zustand passes the stored state and its stored version (0) tomigrate; the returned state uses the current shape. Because the migration ran, the middleware persists the migrated value undersettings. -
In your application, replace the example's seeded
localStoragevalue with the data your earlier release wrote. Keep the migration's return value compatible with the current store, and incrementversionagain for the next breaking shape change.
Options that matter
| Option | Type | Default | What it does |
|---|---|---|---|
name | string | — | Selects the storage key. It is required and must be unique. |
version | number | 0 | Identifies the current persisted shape. A mismatch with the stored version triggers migrate; without a migration function, the stored value is not used. |
migrate | (persistedState: unknown, version: number) => PersistedState | Promise<PersistedState> | (persistedState) => persistedState | Converts the stored state from its recorded version to the current shape. It can return the result synchronously or asynchronously. |
Pitfalls
versionis compared with the version stored alongside the state, not inferred from the fields. Change it when the persisted shape changes.- Return the latest persisted shape from
migrate; do not return the old field name. - A migration runs during hydration. With asynchronous storage, the component can render its initial state before hydration finishes; wait for hydration when that temporary state matters. See Persist store data.
- Persist merges the migrated value with the current state shallowly by default. For partially persisted nested objects, use a custom merge as described in Merge persisted state.
Related
- Persist store data — configure persistence and storage.
- Persist selected state — persist only selected fields.
- Merge persisted state — preserve nested fields during hydration.
Was this page helpful?