React: State Management: Zustand / Redux

Last updated: 2026-08-26

Tom’s shopping cart feature was initially implemented by passing data layer by layer using useState and props, which was barely functional. However, as the component tree grew deeper (App → Header → CartIcon, App → ProductList → ProductCard → AddToCartButton), modifying the shopping cart state required passing retorno de chamada functions through five layers of components—and even the slightest mistake could result in a retorno de chamada being missed or passed incorrectly. He realized: A global state management solution was needed, allowing any component to directly read and write shared state.


1. What You'll Learn



2. Conceptual Diagrams

100%
flowchart LR
    subgraph Zustand[Zustand Pattern]
        A1[create API] --> B1[Store<br/>state + actions]
        B1 --> C1[Component A<br/>Subscribe Now]
        B1 --> C2[Component B<br/>Subscribe Now]
        B1 --> C3[Component C<br/>Subscribe Now]
    end

    subgraph Redux[Redux Toolkit Pattern]
        A2[createSlice] --> B2[Slice<br/>reducers + actions]
        B2 --> C4[configureStore]
        C4 --> D[Provider<br/>Wrapper Root Component]
        D --> E1[useSelector<br/>Read Status]
        D --> E2[useDispatch<br/>Trigger an update]
    end

    style A1 fill:#e1f5fe,stroke:#0288d1
    style B1 fill:#fff3e0,stroke:#f57c00
    style C4 fill:#e8f5e9,stroke:#388e3c
    style D fill:#c8e6c9,stroke:#2e7d32

Left: Zustand—no provider, direct subscriptions, and a minimalist API. Right: Redux Toolkit—offers a complete workflow and is suitable for collaboration among large teams.



3. A Real-Life Scenario

Tom's shopping cart requires several features: an "Add to Cart" button on the product list page, a shopping cart icon in the header (displaying the number of items), a shopping cart page (displaying the product list and total price), and the ability to clear the shopping cart after placing an order. These components are spread across different levels of the component tree, and passing them using useState + props is not only cumbersome but also prone to errors.

(1) Why can't Context be used as a substitute?

React's Context does indeed prevent props from being passed down through multiple levels, but it has two serious problems:

Dedicated state management libraries (Zustand, Redux Toolkit) address performance issues through a selective subscription mechanism—components only listen for the state fragments that concern them, and irrelevant updates do not trigger a re-render.

Solution Performance Provider Selective Subscription Learning Curve Package Size
useState + props ✅ Fast locally None N/A Very low 0
Context + useReducer ❌ Full re-render Required Low 0
Status ✅ Optional subscription Not required ✅ Native Low ~1KB
Redux Toolkit ✅ useSelector Required in ~12KB

(2) Zustand: Minimalist Approach

The core idea behind Zustand is one hook does it all: create() It defines both state and actions at the same time, and the returned hook can be used directly in any component without needing to be wrapped in a Provider.

BASH
npm install zustand

▶ Example 1: Zustand Shopping Cart Store

Output:

TEXT 📖 Display only
Subheading: "Shopping Cart". Buttons: addItem(product)}>Add to Cart, removeItem(item.id)}>Delete, Empty Cart. Text input field
JSX
import { create } from 'zustand'

// Definition store Types of(TypeScript Project Recommendations)
/*
type CartItem = { id: number; name: string; price: number; qty: number }
type CartStore = {
  items: CartItem[]
  total: number
  addItem: (product: Omit<CartItem, 'qty'>) => void
  removeItem: (id: number) => void
  updateQty: (id: number, qty: number) => void
  clearCart: () => void
}
*/

// Create store — state and  actions They're all defined here
const useCartStore = create((set, get) => ({
  // --- state ---
  items: [],
  total: 0,

  // --- actions ---
  addItem: (product) => set((state) => {
    const existing = state.items.find(i => i.id === product.id)
    const newItems = existing
      ? state.items.map(i =>
          i.id === product.id ? { ...i, qty: i.qty + 1 } : i
        )
      : [...state.items, { ...product, qty: 1 }]
    return {
      items: newItems,
      total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
    }
  }),

  removeItem: (id) => set((state) => {
    const newItems = state.items.filter(i => i.id !== id)
    return {
      items: newItems,
      total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
    }
  }),

  updateQty: (id, qty) => set((state) => {
    const newItems = qty <= 0
      ? state.items.filter(i => i.id !== id)
      : state.items.map(i => i.id === id ? { ...i, qty } : i)
    return {
      items: newItems,
      total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
    }
  }),

  clearCart: () => set({ items: [], total: 0 })
}))

// Use in any component
function CartIcon() {
  // Subscribe to items.length only,The page will only be re-rendered when the shopping cart list changes.
  const itemCount = useCartStore(state => state.items.length)

  return <span className="cart-badge">Shopping Cart ({itemCount})</span>
}

function ProductCard({ product }) {
  const addItem = useCartStore(state => state.addItem)

  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => addItem(product)}>Add to Cart</button>
    </div>
  )
}

function CartPage() {
  const { items, total, updateQty, removeItem, clearCart } = useCartStore()

  if (items.length === 0) return <p>Your shopping cart is empty</p>

  return (
    <div>
      <h2>Shopping Cart</h2>
      {items.map(item => (
        <div key={item.id}>
          <span>{item.name}</span>
          <input
            type="number"
            value={item.qty}
            min="0"
            onChange={e => updateQty(item.id, Number(e.target.value))}
          />
          <span>${item.price * item.qty}</span>
          <button onClick={() => removeItem(item.id)}>Delete</button>
        </div>
      ))}
      <hr />
      <p><strong>Total:${total}</strong></p>
      <button onClick={clearCart}>Empty Cart</button>
    </div>
  )
}

Output:

TEXT 📖 Display only
Zustand store: create((set) => ({ count: 0, increment: () => set(s => ({ count: s.count + 1 })) }))

Key Features of Zustand:

▶ Example 2: Zustand Persistence Middleware

Output:

TEXT 📖 Display only
Subheading: "Shopping Cart". Displays: "state.items.length)  return". Buttons: addItem(product)}>Add to Cart, removeItem(item.id)}>Delete, Empty Cart. Input type: number

Tom wants users to be able to refresh the page without losing their shopping cart data. The persist middleware in Zustand can automatically sync the store to localStorage.

JSX
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

// persist Middleware Package store Definition
const useCartStore = create(
  persist(
    (set, get) => ({
      items: [],
      total: 0,
      addItem: (product) => set((state) => {
        const existing = state.items.find(i => i.id === product.id)
        const newItems = existing
          ? state.items.map(i =>
              i.id === product.id ? { ...i, qty: i.qty + 1 } : i
            )
          : [...state.items, { ...product, qty: 1 }]
        return {
          items: newItems,
          total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
        }
      }),
      removeItem: (id) => set((state) => {
        const newItems = state.items.filter(i => i.id !== id)
        return {
          items: newItems,
          total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
        }
      }),
      clearCart: () => set({ items: [], total: 0 })
    }),
    {
      name: 'cart-storage',  // localStorage 's  key
      // Store all fields by default,That's fine, too. partialize Select which fields to store
      // partialize: (state) => ({ items: state.items }),
    }
  )
)

// The way it works remains exactly the same
function CartIcon() {
  const itemCount = useCartStore(state => state.items.length)
  return <span>Shopping Cart ({itemCount})</span>
}

// localStorage Data Format Stored in the System:
// key: 'cart-storage'
// value: '{"state":{"items":[...],"total":99},"version":0}'
▶ Try it Yourself

How Persistence Middleware Works: Any state changes are automatically synchronized to localStorage, and data is automatically restored from localStorage when the application starts. When the user refreshes the page, the shopping cart state is fully preserved.

▶ Example 3: Debugging in Zustand DevTools

Output:

TEXT 📖 Display only
Zustand DevTools: view state change log, action history, and time-travel debug. Install Redux DevTools extension to inspect store

During the development phase, Tom wanted to view the history of state changes, similar to Redux DevTools. Zustand’s devtools middleware integrates seamlessly with browser DevTools extensions.

JSX
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

// Using Multiple Middleware Components Simultaneously:Combine using a nested approach
const useStore = create(
  devtools(
    persist(
      (set) => ({
        count: 0,
        increment: () => set(state => ({ count: state.count + 1 })),
        decrement: () => set(state => ({ count: state.count - 1 })),
      }),
      { name: 'app-storage' }
    ),
    { name: 'AppStore' }  // DevTools As shown in store Name
  )
)

// Open in a browser Redux DevTools Expand
// As you can see "AppStore" All items under this tag state Change Log
// Supports time-travel debugging(Jump to any historical state)
▶ Try it Yourself

Output:

TEXT 📖 Display only
Zustand DevTools: view state change log, action history, and time-travel debug. Install Redux DevTools extension to inspect store

Features of the Zustand middleware system: Middleware is executed sequentially from the outer layer to the inner layer through nested function combinations. devtools(persist(...)) indicates that DevTools is connected first, followed by persistence processing. Multiple middleware components can be flexibly combined to meet different needs. For example, you can add the immer middleware to enable variable syntax, or create custom middleware to log each action.



4. Redux Toolkit: An Enterprise-Grade Solution

When a project grows to the point where multiple teams are collaborating, the flexibility of Zustand can actually become a problem—developers may write actions in inconsistent styles. Redux Toolkit provides a standardized state management workflow that enforces a consistent pattern across the team.

BASH
npm install @reduxjs/toolkit react-redux

▶ Example 4: The Complete Redux Toolkit Workflow

Output:

TEXT 📖 Display only
Subheading: "Counter:{value}". Buttons: dispatch(increment())}>+1, dispatch(decrement())}>-1, dispatch(incrementByAmount(5))}>+5, dispatch(reset())}>Reset. List items: {h}
JSX
import { createSlice, configureStore } from '@reduxjs/toolkit'
import { Provider, useSelector, useDispatch } from 'react-redux'

// ========== 1. Create Slice(Definition state + reducers)==========
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0, history: [] },
  reducers: {
    increment: (state) => {
      state.value += 1
      state.history.push(`+1 → ${state.value}`)
    },
    decrement: (state) => {
      state.value -= 1
      state.history.push(`-1 → ${state.value}`)
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
      state.history.push(`+${action.payload} → ${state.value}`)
    },
    reset: (state) => {
      state.value = 0
      state.history = []
    }
  }
})

// Automatically generated action creators
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions

// ========== 2. Layout Store ==========
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    // More can be added later. slice:
    // cart: cartSlice.reducer,
    // user: userSlice.reducer,
  }
})

// ========== 3. Provider Inject ==========
function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  )
}

// ========== 4. Used in components ==========
function Counter() {
  // Selective Reading of Characters
  const value = useSelector(state => state.counter.value)
  const history = useSelector(state => state.counter.history)
  const dispatch = useDispatch()

  return (
    <div>
      <h2>Counter:{value}</h2>
      <div>
        <button onClick={() => dispatch(increment())}>+1</button>
        <button onClick={() => dispatch(decrement())}>-1</button>
        <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
        <button onClick={() => dispatch(reset())}>Reset</button>
      </div>
      <div>
        <h3>Transaction History</h3>
        <ul>
          {history.slice(-5).map((h, i) => (
            <li key={i}>{h}</li>
          ))}
        </ul>
      </div>
    </div>
  )
}

Output:

TEXT 📖 Display only
Redux store: createSlice defines actions (increment, decrement). configureStore creates store. useSelector reads state, useDispatch sends actions.

Key Concepts of Redux Toolkit:

Concept Function Analogy
createSlice Create a set of related states and reducers File Drawer: Organize and Store
configureStore Combine all slices to create a global store File Cabinet: Centralized Management
Provider Injecting the store into the React component tree Power cord: Plug it in
useSelector Read a state fragment from the store Open the drawer to retrieve a file
useDispatch Trigger an action to update the status Issue a command for the administrator to take action

▶ Example 5: Asynchronous Operations in Redux Toolkit

Output:

TEXT 📖 Display only
Buttons: dispatch(fetchuser(userid))}>load users

State updates in real-world projects often involve asynchronous logic (such as logging in or retrieving user information). Redux Toolkit includes built-in support for the lifecycle stages of asynchronous operations (pending, fulfilled, and rejected).

JSX
import { createSlice, configureStore, createAsyncThunk } from '@reduxjs/toolkit'
import { Provider, useSelector, useDispatch } from 'react-redux'

// 1. Defining Asynchronous thunk(Automatically Generated pending/fulfilled/rejected Three types action)
const fetchUser = createAsyncThunk(
  'user/fetchUser',      // action Type prefix
  async (userId, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/users/${userId}`)
      if (!response.ok) throw new Error('Failed to retrieve the user')
      return await response.json()
    } catch (err) {
      return rejectWithValue(err.message)
    }
  }
)

// 2. Create slice
const userSlice = createSlice({
  name: 'user',
  initialState: {
    data: null,
    loading: false,
    error: null
  },
  reducers: {
    clearUser: (state) => {
      state.data = null
      state.error = null
    }
  },
  // Handling Asynchronous Operations thunk The Three States of
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true
        state.error = null
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false
        state.data = action.payload
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false
        state.error = action.payload
      })
  }
})

export const { clearUser } = userSlice.actions

// 3. Layout store
const store = configureStore({
  reducer: { user: userSlice.reducer }
})

// 4. Used in components
function UserProfile({ userId }) {
  const dispatch = useDispatch()
  const { data, loading, error } = useSelector(state => state.user)

  return (
    <div>
      <button onClick={() => dispatch(fetchUser(userId))}>Load Users</button>
      {loading && <p>Loading......</p>}
      {error && <p>Error:{error}</p>}
      {data && <p>{data.name} — {data.email}</p>}
    </div>
  )
}

createAsyncThunk automatically handles the three states: When a request is initiated, it dispatches pending → Upon success, it dispatches fulfilled (carrying the data) → Upon failure, it dispatches rejected (carrying the error message). Developers only need to define how the state changes for each condition within extraReducers. Compared to using useEffect and fetch directly, the advantages of this pattern include: automatic tracking of request status, actions that can be logged by DevTools, and the ability for multiple components to subscribe to the results of the same thunk.



5. Model Comparison

Dimension State Redux Toolkit
Code volume Very little—just one create() function Moderate—the full suite of slice, store, and Provider
Learning Curve Low (10 minutes to get started) Medium (requires understanding of reducers, actions, dispatch, and thunks)
Provider Not required Must be a Provider package
Selective Subscription Native Support, Selector Functions useSelector + Shallow Comparison
TypeScript Native-friendly Good, with type inference
Middleware persist / devtools / immer, etc. Built-in thunk + extensible saga/epic
Asynchronous processing Implemented manually in the action Built-in with createAsyncThunk
Package Size ~1KB ~12KB
Use Cases Small and medium-sized applications, state sharing between components Large applications, complex data flows, cross-team collaboration
DevTools Supported (requires middleware) Native support, time-travel debugging

Selection Tips:


❓ FAQ

Q How do I choose between Zustand and Context + useReducer?
A Zustand offers better performance—when Context is updated, all consumers are re-rendered, whereas Zustand supports fine-grained, selective subscriptions. Zustand requires less code and avoids nested Providers. Context + useReducer is suitable for extremely simple scenarios (such as theme switching), but in most cases, Zustand is the better choice.
Q What does Redux Toolkit simplify compared to traditional Redux?
A Traditional Redux requires manually writing action type constants, action creators, and reducer switch-case statements, as well as separately configuring Redux DevTools and middleware. Redux Toolkit’s createSlice automatically generates action types and action creators, configureStore automatically integrates middleware and DevTools, and createAsyncThunk simplifies asynchronous workflows. This reduces the amount of code by about 60%.
Q Can the Zustand store be used outside of components?
A Yes. useCartStore.getState() Retrieve the current state from anywhere, useCartStore.setState(...) update the state from anywhere. This is very useful for accessing the state in route guards, interceptors, and WebSocket callbacks.
Q Can both libraries be used in the same project at the same time?
A Absolutely. Many projects start with Zustand and later introduce Redux Toolkit when the complexity of a particular module increases. The two do not conflict—Zustand manages simple global state (themes, user information), while Redux Toolkit manages complex business modules (order processes, approval workflows).
Q Should you only use a state management library when useReducer doesn’t meet your needs?
A useReducer is suitable for complex state logic within a single component or in a localized scope. You should only introduce Zustand or Redux Toolkit when state needs to be shared across multiple unrelated components, when it needs to be persisted, or when you require middleware. Don’t use them just for the sake of using them.
Q What is the difference between Zustand’s set function and React’s setState?
A Zustand’s set performs shallow merging by default (similar to class components’ setState), while Redux Toolkit’s reducers, based on Immer, perform deep, immutable updates. Zustand can also be used with the immer middleware to achieve mutable update syntax similar to Redux Toolkit. Both approaches have their pros and cons in terms of usage; choose based on your team’s preferences.

📖 Summary


📝 Exercises

  1. Create a Todo Store using Zustand: It should support adding tasks, changing the completion status, deleting tasks, clearing completed tasks, and counting the number of uncompleted tasks. Use it in both the list component and the statistics component to verify the effectiveness of selective subscription.
  2. Add the persist middleware to Todo Store to verify whether the data is retained after refreshing the page.
  3. Implement a user management module using Redux Toolkit: Use createAsyncThunk to simulate fetching a list of users (with a 1-second delay before returning data), manage the three states (loading, error, and data), and display the loading state, error state, and data list in the component.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏