Skip to content
D
Documentation

Migrate persisted state

how-to
2 min readUpdated

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

  1. Install Zustand in your application:

    bash
    npm install zustand
    
  2. Set the storage item to the old shape and create the store with a new version. This complete React example renames oldLayout to layout:

    tsx
    import { 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) to migrate; the returned state uses the current shape. Because the migration ran, the middleware persists the migrated value under settings.

  3. In your application, replace the example's seeded localStorage value with the data your earlier release wrote. Keep the migration's return value compatible with the current store, and increment version again for the next breaking shape change.

Options that matter

OptionTypeDefaultWhat it does
namestring—Selects the storage key. It is required and must be unique.
versionnumber0Identifies 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) => persistedStateConverts the stored state from its recorded version to the current shape. It can return the result synchronously or asynchronously.

Pitfalls

  • version is 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.

Was this page helpful?