# How to Persist Zustand State to localStorage or sessionStorage Using the Persist Middleware

> Easily persist Zustand state to localStorage or sessionStorage using the built-in persist middleware. Learn to save and load your application's state automatically.

- Repository: [Poimandres/zustand](https://github.com/pmndrs/zustand)
- Tags: how-to-guide
- Published: 2026-03-06

---

**The Zustand persist middleware wraps your store creator to automatically synchronize state with browser storage, using `window.localStorage` by default while allowing you to swap in `sessionStorage` or custom adapters via the `storage` option.**

In the `pmndrs/zustand` repository, the persist middleware provides a declarative way to make your global state survive page reloads. By intercepting the store initializer, it handles serialization, storage I/O, and rehydration without requiring boilerplate in your components.

## Architecture of the Persist Middleware

The middleware is implemented in [[`src/middleware/persist.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/persist.ts)](https://github.com/pmndrs/zustand/blob/main/src/middleware/persist.ts). It operates by wrapping the vanilla store creator and injecting a storage synchronization layer.

### Storage Adapter Creation

At its core, the middleware uses **createJSONStorage**, defined at lines 31-61, to normalize any storage object into a JSON-aware interface:

```typescript
export function createJSONStorage<S, R = unknown>(
  getStorage: () => StateStorage<R>,
  options?: JsonStorageOptions,
): PersistStorage<S, unknown> | undefined { … }

```

By default, `persist` initializes this with `window.localStorage`:

```typescript
// Default storage initialization (persist.ts ~line 89-91)
let options = {
  storage: createJSONStorage<S, void>(() => window.localStorage),
  // … other defaults
}

```

If the getter throws (e.g., during server-side rendering where `window` is undefined), the middleware gracefully falls back to no-op behavior.

### Hydration and Synchronization

When the store mounts, the middleware executes a **hydrate** cycle:

1. **Read** – Retrieves the stored JSON string using `storage.getItem`.
2. **Migrate** – If a `version` mismatch is detected, runs the user-provided `migrate` function.
3. **Merge** – Combines the persisted slice with the fresh initial state using the optional `merge` strategy.
4. **Subscribe** – Attaches a listener to `setState` that writes back to storage on every change, applying `partialize` to strip non-persistable fields.

The middleware also exposes a control API on `store.persist`, including `setOptions`, `clearStorage`, `rehydrate`, and lifecycle hooks like `onFinishHydration`.

## Persisting to localStorage (Default Behavior)

To persist state across browser sessions, import `persist` and provide a unique `name` key:

```typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export const useSettingsStore = create(
  persist(
    (set) => ({
      theme: 'dark',
      toggleTheme: () => set((state) => ({ 
        theme: state.theme === 'dark' ? 'light' : 'dark' 
      })),
    }),
    {
      name: 'user-settings', // Key in localStorage
      partialize: (state) => ({ theme: state.theme }), // Store only theme
    }
  )
)

```

The **partialize** option accepts a function that receives the full state and returns a shallow object containing only the fields you want serialized. By default, the entire state is stringified and written to `localStorage` after every update.

## Switching to sessionStorage

To use `sessionStorage` instead, pass a custom `storage` option using `createJSONStorage`:

```typescript
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

export const useSessionStore = create(
  persist(
    (set) => ({
      cart: [] as string[],
      addItem: (id) => set((state) => ({ cart: [...state.cart, id] })),
    }),
    {
      name: 'shopping-cart',
      storage: createJSONStorage(() => window.sessionStorage),
    }
  )
)

```

Because `sessionStorage` shares the same interface as `localStorage` (`getItem`, `setItem`, `removeItem`), `createJSONStorage` handles JSON serialization automatically. This pattern works for any custom storage object implementing the `StateStorage` interface.

## Advanced Configuration: Versioning and Migration

For long-lived applications where state shape evolves, the middleware supports schema versioning:

```typescript
export const useVersionedStore = create(
  persist(
    (set) => ({
      user: { id: '', name: '', email: '' },
      setUser: (user) => set({ user }),
    }),
    {
      name: 'user-store',
      version: 2, // Current schema version
      migrate: (persistedState, version) => {
        if (version === 1) {
          // Migrate from v1: add email field
          return { 
            ...persistedState, 
            user: { ...persistedState.user, email: '' } 
          }
        }
        return persistedState
      },
    }
  )
)

```

The **migrate** function receives the stored state and its version number, allowing you to transform legacy data before it reaches your components. If omitted, the middleware overwrites old data with the fresh initial state when versions differ.

## SSR-Safe Hydration with skipHydration

In server-side rendering environments, attempting to access `localStorage` during the initial render throws an error. The **skipHydration** option defers rehydration until the client bundle executes:

```typescript
export const useSSRStore = create(
  persist(
    (set) => ({
      data: null,
      setData: (d) => set({ data: d }),
    }),
    {
      name: 'ssr-store',
      skipHydration: true, // Prevents automatic rehydration
    }
  )
)

// In your client-side component:
useEffect(() => {
  useSSRStore.persist.rehydrate()
}, [])

```

Calling `store.persist.rehydrate()` manually triggers the same read-and-merge logic that normally runs automatically on mount.

## Runtime Storage Management

The middleware attaches utility methods to `store.persist` for imperative control:

- **`clearStorage()`** – Removes the stored entry entirely.
- **`setOptions(newOptions)`** – Dynamically changes configuration, including swapping storage targets.
- **`hasHydrated()`** – Returns a boolean indicating whether hydration has completed.
- **`onFinishHydration(callback)`** – Registers a listener that fires once when hydration finishes.

Example of clearing persisted data on user logout:

```typescript
const logout = () => {
  useSessionStore.persist.clearStorage()
  // Redirect to login...
}

```

## Summary

- The persist middleware in [[`src/middleware/persist.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/persist.ts)](https://github.com/pmndrs/zustand/blob/main/src/middleware/persist.ts) wraps stores to sync state with browser storage.
- **createJSONStorage** adapts `localStorage`, `sessionStorage`, or custom storage objects to handle JSON serialization.
- Configure persistence with `name`, `storage`, `partialize`, `version`, and `migrate` options.
- Use `skipHydration` for SSR compatibility, then call `store.persist.rehydrate()` client-side.
- Access `store.persist.clearStorage()` and other controls for runtime storage management.

## Frequently Asked Questions

### How do I persist only specific fields of my Zustand store?

Use the **partialize** option in your persist configuration. This function receives the entire state object and should return a shallow object containing only the fields you want stored. Fields omitted from the returned object are not written to storage and reset to their initial values on reload.

### Can I use sessionStorage instead of localStorage with Zustand persist?

Yes. Pass `storage: createJSONStorage(() => window.sessionStorage)` in your persist options. The `createJSONStorage` helper accepts any object implementing `getItem`, `setItem`, and `removeItem`, making it compatible with `sessionStorage`, `localStorage`, or custom async storage wrappers.

### What happens if I change the structure of my stored state?

Specify a **version** number in your persist options and provide a **migrate** function. When the middleware detects a version mismatch during hydration, it executes your migration logic to transform the old data shape into the new one before merging it with the store.

### How do I prevent hydration errors during server-side rendering?

Set **skipHydration: true** in the persist options. This prevents the middleware from attempting to read `localStorage` during the server render. Then, in a client-side `useEffect`, call `store.persist.rehydrate()` to load the stored state once the browser environment is available.