# Migrate persisted state

Use versioning when you change the schema stored by [`persist`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#persist), such as moving flat coordinates into a nested `position` object. Add a custom `merge` when stored objects omit fields that your current store supplies as defaults.

This browser example migrates version 0 data into version 1 and renders a red dot at `x: 40, y: 100`. The stored `y` survives migration; the missing `x` comes from the current store's nested defaults.

## 1. Version the store and migrate the old schema

Keep the [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) and [`createJSONStorage`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand-middleware#createjsonstorage) setup from [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data); add `version`, `migrate`, and `merge` to handle the schema change below.

In your React TypeScript project, install Zustand:

```bash
npm install zustand
```

Create `position-store.ts`. The seed represents an older, partially persisted schema and only runs when this example's storage key is absent. Remove the seeding code from your application after testing the migration.

```ts title="position-store.ts"
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'

type PositionStore = {
  position: { x: number; y: number }
  moveRight: () => void
}

type PersistedPosition = {
  position: Partial<PositionStore['position']>
}

// Browser demonstration: seed an older schema before creating the store.
const storageName = 'position-migration-example'
if (localStorage.getItem(storageName) === null) {
  localStorage.setItem(
    storageName,
    JSON.stringify({ state: { y: 100 }, version: 0 }),
  )
}

export const usePositionStore = create<PositionStore>()(
  persist(
    (set) => ({
      position: { x: 40, y: 40 },
      moveRight: () =>
        set((state) => ({
          position: { ...state.position, x: state.position.x + 20 },
        })),
    }),
    {
      name: storageName,
      storage: createJSONStorage<PersistedPosition>(() => localStorage),
      version: 1,
      partialize: (state): PersistedPosition => ({
        position: state.position,
      }),
      migrate: (persisted, version): PersistedPosition => {
        if (
          version === 0 &&
          typeof persisted === 'object' &&
          persisted !== null
        ) {
          return {
            position: {
              ...('x' in persisted && typeof persisted.x === 'number'
                ? { x: persisted.x }
                : {}),
              ...('y' in persisted && typeof persisted.y === 'number'
                ? { y: persisted.y }
                : {}),
            },
          }
        }
        // This example supports only the version 0 migration.
        return { position: {} }
      },
      merge: (persisted, current) => {
        if (
          typeof persisted !== 'object' ||
          persisted === null ||
          !('position' in persisted) ||
          typeof persisted.position !== 'object' ||
          persisted.position === null
        ) {
          return current
        }
        const position = persisted.position
        return {
          ...current,
          position: {
            ...current.position,
            ...('x' in position && typeof position.x === 'number'
              ? { x: position.x }
              : {}),
            ...('y' in position && typeof position.y === 'number'
              ? { y: position.y }
              : {}),
          },
        }
      },
    },
  ),
)
```

`migrate` receives the stored state and its stored version, not the target version. Return data compatible with the current persisted schema; the callback can also return a promise. Here it moves the old `x` and `y` fields into `position`, without inventing values for missing coordinates.

Hydration calls `merge` with the migrated data and the current state. After a successful migration, `persist` writes the merged, filtered state back to storage with version 1. `partialize` stores only `position`, leaving the action out of the stored data.

## 2. Preserve nested defaults and render the result

The custom `merge` copies `current.position` first, then overlays valid stored coordinates. It returns the full store, including `moveRight`.

The default shallow merge can erase unpersisted nested fields: a stored `{ position: { y: 100 } }` replaces the entire current `position` object. Merge that nested object explicitly, as above, or use a deep merge for a larger schema. Keep the current state as the base and let persisted fields override it.

Mount a component that selects the position and action separately:

```tsx title="main.tsx"
import { createRoot } from 'react-dom/client'
import { usePositionStore } from './position-store'

function App() {
  const position = usePositionStore((state) => state.position)
  const moveRight = usePositionStore((state) => state.moveRight)

  return (
    <main>
      <p>x: {position.x}, y: {position.y}</p>
      <button onClick={moveRight}>Move right</button>
      <div
        style={{
          position: 'relative',
          height: 240,
          width: 400,
          border: '1px solid gray',
        }}
      >
        <div
          style={{
            position: 'absolute',
            left: position.x,
            top: position.y,
            width: 20,
            height: 20,
            borderRadius: '50%',
            background: 'red',
          }}
        />
      </div>
    </main>
  )
}

createRoot(document.getElementById('root')!).render(<App />)
```

Reuse the `index.html` root element from [React quick start](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/react-quick-start#3-mount-the-counter), with the module script's `src` set to `/main.tsx` to load this migration example.

On the first run with an absent example key, the readout shows `x: 40, y: 100`, and the red dot appears at those offsets inside the bordered container. Click **Move right** to increase `x` by 20. Reload to see the saved position restored; version 1 data goes through `merge` without running `migrate` again.

## Options that matter

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `name` | `string` | Required | Sets the unique storage key. Keep the key when changing the schema so migration can read the old data. |
| `version` | `number` | `0` | Sets the version written with persisted state. A different numeric stored version triggers migration. |
| `migrate` | `(persistedState: unknown, version: number) => PersistedState \| Promise<PersistedState>` | Not provided | Converts mismatched-version data to the current persisted schema. Without it, mismatched-version data is not used. |
| `merge` | `(persistedState: unknown, currentState: State) => State` | `{ ...currentState, ...persistedState }` | Combines hydrated data with the current store. Supply a nested merge to retain omitted defaults. |
| `partialize` | `(state: State) => PersistedState` | `(state) => state` | Filters what is written to storage. |

In the table, `State` and `PersistedState` denote your store and persisted-data types.

## Check the migration boundary

- Increment `version` for a breaking storage-schema change and handle each supported old version in `migrate`. The example handles version 0; other mismatched versions return an empty position, so the merge retains current defaults.
- Migration runs only when the stored version is a number different from the configured version. A missing version does not trigger it. The custom merge still handles same-version partially persisted objects.
- These files run in the browser and use synchronous `localStorage`. For asynchronous storage and initial-render behavior, see [Persist store data](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/persist-store-data). For manual hydration, see [Control persistence hydration](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/control-persistence-hydration).

## Related

- [Update nested state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/update-nested-state) — change nested state after hydration.
- [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware) — type middleware composition.
