# How to Migrate from Redux to Zustand: A Complete Guide for React Developers

> Migrate from Redux to Zustand easily for React applications. Simplify state management by replacing Redux concepts with Zustand's flexible create and useStore hooks.

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

---

**You can migrate from Redux to Zustand by replacing `createStore` with Zustand's `create` function, eliminating action type constants in favor of direct `set` calls, and using the `useStore` hook instead of `useSelector`—all without needing a Provider wrapper.**

Zustand is a tiny, unopinionated state-management library that replaces Redux with significantly less boilerplate while preserving core concepts like single-store architecture and immutable updates. This guide walks through the migration process using the actual implementation details from the `pmndrs/zustand` repository.

## Step-by-Step Migration Strategy

### Replace Redux Store Creation with Zustand's `create`

In Redux, you initialize state using `createStore(reducer, preloadedState)`. In Zustand, you use the `create` function exported from the library, which is implemented in **[`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts)**【/src/vanilla.ts】.

```typescript
// Redux approach
import { createStore } from 'redux'
const store = createStore(reducer, { count: 0 })

// Zustand approach
import { create } from 'zustand'
const useStore = create((set) => ({
  count: 0,
  increment: (n) => set((state) => ({ count: state.count + n })),
  decrement: (n) => set((state) => ({ count: state.count - n })),
}))

```

The `create` function builds a store with a `setState` method that performs shallow merges, eliminating the need for `combineReducers` or switch statements.

### Remove Action Type Constants and Action Creators

Redux requires dispatching action objects with type constants. Zustand allows direct state updates via the `set` function provided in the store initializer.

```typescript
// Redux: Action types and creators
const ADD_TODO = 'todos/add'
const addTodo = (text) => ({ type: ADD_TODO, payload: text })
store.dispatch(addTodo('Buy milk'))

// Zustand: Direct set calls
const useStore = create((set) => ({
  todos: [],
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { text, id: Date.now() }]
  })),
}))

```

If you prefer keeping the reducer pattern during migration, Zustand provides a `redux` middleware in **[`src/middleware/redux.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/redux.ts)**【/src/middleware/redux.ts】 that injects a `dispatch` method forwarding actions to your existing reducer.

### Update Component Subscriptions

Redux uses `useSelector` and `useDispatch` hooks within a Provider context. Zustand's React binding in **[`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts)**【/src/react.ts】 uses `useSyncExternalStore` under the hood, allowing components to subscribe directly to the store without a Provider wrapper.

```tsx
// Redux component
import { useSelector, useDispatch } from 'react-redux'
const Counter = () => {
  const count = useSelector((state) => state.count)
  const dispatch = useDispatch()
  return <button onClick={() => dispatch({ type: 'inc' })}>{count}</button>
}

// Zustand component
import { useCounter } from './store'
const Counter = () => {
  const count = useCounter((s) => s.count)
  const inc = useCounter((s) => s.inc)
  return <button onClick={inc}>{count}</button>
}

```

## Keeping the Reducer Pattern with Redux Middleware

For teams wanting to migrate incrementally, Zustand's `redux` middleware preserves your existing reducer logic while transitioning to Zustand's store model.

```typescript
// zustandWithRedux.ts
import { create } from 'zustand'
import { redux } from 'zustand/middleware'

type State = { count: number }
type Action = { type: 'increment' | 'decrement'; amount: number }

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + action.amount }
    case 'decrement':
      return { count: state.count - action.amount }
    default:
      return state
  }
}

export const useCounter = create(
  redux(reducer, { count: 0 })  // Implementation in src/middleware/redux.ts
)

// Usage preserves Redux dispatch API
useCounter.getState().dispatch({ type: 'increment', amount: 5 })

```

This approach allows you to maintain existing action creators and reducer tests while benefiting from Zustand's smaller bundle size and simpler React integration.

## Core Architectural Differences

Understanding these differences helps inform your migration strategy:

| Feature | Redux | Zustand |
|---------|-------|---------|
| **Store Creation** | `createStore(reducer, preloadedState)` outside React | `create(initializer)` in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) returns a bound store usable anywhere |
| **State Updates** | Pure reducers + `dispatch(action)` → new state | `set` merges partial objects; optional `redux` middleware in [`src/middleware/redux.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/redux.ts) keeps reducer pattern |
| **Middleware** | `applyMiddleware(...middlewares)` | Higher-order `StateCreator` functions (e.g., `devtools`, `persist`, `redux`) |
| **React Integration** | `Provider` + `useSelector`, `useDispatch` | `useStore(store, selector?)` using `useSyncExternalStore` in [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts); no Provider needed |
| **DevTools** | Requires `composeWithDevTools` setup | `devtools` middleware connects automatically |

The official comparison documentation in **[`docs/learn/getting-started/comparison.md`](https://github.com/pmndrs/zustand/blob/main/docs/learn/getting-started/comparison.md)**【/docs/learn/getting-started/comparison.md】 provides additional context on performance characteristics and render optimization strategies.

## Complete Migration Example

Here's a before-and-after comparison for a todo application:

**Redux Implementation:**

```typescript
// store.ts
import { createStore } from 'redux'

type Todo = { id: number; text: string; done: boolean }
type State = { todos: Todo[] }
type Action = 
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: number }

const reducer = (state: State = { todos: [] }, action: Action): State => {
  switch (action.type) {
    case 'ADD_TODO':
      return { todos: [...state.todos, { id: Date.now(), text: action.payload, done: false }] }
    case 'TOGGLE_TODO':
      return { todos: state.todos.map(t => t.id === action.payload ? { ...t, done: !t.done } : t) }
    default:
      return state
  }
}

export const store = createStore(reducer)

```

**Zustand Migration:**

```typescript
// store.ts
import { create } from 'zustand'

type Todo = { id: number; text: string; done: boolean }

interface TodoStore {
  todos: Todo[]
  addTodo: (text: string) => void
  toggleTodo: (id: number) => void
}

export const useTodoStore = create<TodoStore>((set) => ({
  todos: [],
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), text, done: false }]
  })),
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(t => t.id === id ? { ...t, done: !t.done } : t)
  })),
}))

```

## Summary

- **Store initialization** moves from `createStore(reducer)` to `create(initializer)` in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts), eliminating the need for `combineReducers` and Provider wrappers.
- **State updates** replace `dispatch(action)` with direct `set` calls that merge partial state, though the `redux` middleware in [`src/middleware/redux.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/redux.ts) preserves existing reducer logic during migration.
- **React integration** switches from `useSelector` and `useDispatch` to `useStore(store, selector?)` implemented in [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts) using `useSyncExternalStore`, removing the need for context providers.
- **Middleware architecture** shifts from Redux's `applyMiddleware` to higher-order `StateCreator` functions that wrap the store initializer.

## Frequently Asked Questions

### Can I keep my existing Redux reducers when migrating to Zustand?

Yes. Zustand provides a `redux` middleware in **[`src/middleware/redux.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/redux.ts)** that accepts your existing reducer function and initial state, injecting a `dispatch` method into the store. This allows you to maintain your current action creators and reducer tests while transitioning to Zustand's simpler store model.

### Do I need to wrap my application in a Provider when using Zustand?

No. Unlike Redux, which requires a `Provider` component to pass the store via React context, Zustand stores are created as module-level exports that can be imported directly into components. The React binding in **[`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts)** uses `useSyncExternalStore` to subscribe to the store without context overhead.

### How do I handle asynchronous logic when migrating from Redux Thunk?

Zustand does not require a separate middleware for asynchronous actions. Instead, you can define async functions directly in your store actions using the `set` parameter. For complex async flows, you can use the `devtools` middleware to track async state changes in Redux DevTools, or implement your own middleware pattern following the higher-order `StateCreator` pattern used in **[`src/middleware/redux.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/redux.ts)**.

### Is Zustand's performance better than Redux for large applications?

According to the comparison documentation in **[`docs/learn/getting-started/comparison.md`](https://github.com/pmndrs/zustand/blob/main/docs/learn/getting-started/comparison.md)**, Zustand achieves similar or better performance through its selector-based subscription model. Components only re-render when the specific slice of state they select changes, similar to Redux's `useSelector` but without the context provider overhead. The `useSyncExternalStore` implementation in **[`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts)** ensures consistent behavior with React's concurrent features.