React: 状态管理:Zustand / Redux

最后更新:2026-08-26

Tom 的购物车功能最初用 useState 加上 props 逐层传递,勉强能用。但随着组件树越来越深(App → Header → CartIcon,App → ProductList → ProductCard → AddToCartButton),修改购物车状态需要在 5 层组件间传递回调函数,稍有不慎就会漏传或错传。他意识到:需要一套全局状态管理方案,让任意组件都能直接读写共享状态


1. 你将学到



2. 概念图解

100%
flowchart LR
    subgraph Zustand[Zustand 模式]
        A1[create API] --> B1[Store<br/>state + actions]
        B1 --> C1[Component A<br/>直接订阅]
        B1 --> C2[Component B<br/>直接订阅]
        B1 --> C3[Component C<br/>直接订阅]
    end

    subgraph Redux[Redux Toolkit 模式]
        A2[createSlice] --> B2[Slice<br/>reducers + actions]
        B2 --> C4[configureStore]
        C4 --> D[Provider<br/>包裹根组件]
        D --> E1[useSelector<br/>读取状态]
        D --> E2[useDispatch<br/>触发更新]
    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

左:Zustand 零 Provider、直接订阅、API 极简。右:Redux Toolkit 有完整流程,适合大团队协作。



3. 一个真实场景

Tom 的购物车需要多个功能:商品列表页的"加入购物车"按钮、页眉的购物车图标(显示商品数量)、购物车页面(显示商品列表和总价)、下单后清空购物车。这些组件分布在组件树的不同层级,用 useState + props 传递不仅啰嗦而且容易出错。

(1) 为什么不能用 Context 替代?

React 的 Context 确实可以避免 props 逐层传递,但它有两个严重问题:

专门的状态管理库(Zustand、Redux Toolkit)通过选择性订阅机制解决了性能问题——组件只监听自己关心的状态片段,无关更新不触发重渲染。

方案 性能 Provider 选择性订阅 学习曲线 包大小
useState + props ✅ 本地快 N/A 极低 0
Context + useReducer ❌ 全量渲染 需要 0
Zustand ✅ 选择性订阅 不需要 ✅ 原生 ~1KB
Redux Toolkit ✅ useSelector 需要 ~12KB

(2) Zustand:极简方案

Zustand 的核心思想是一个 Hook 搞定一切create() 同时定义 state 和 actions,返回的 Hook 可以在任何组件中直接使用,无需 Provider 包裹。

BASH
npm install zustand

▶ 示例 1:Zustand 购物车 Store

JSX 📖 仅展示
import { create } from 'zustand'

// 定义 store 的类型(TypeScript 项目推荐)
/*
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
}
*/

// 创建 store — state 和 actions 都定义在这里
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 })
}))

// 在任意组件中使用
function CartIcon() {
  // 仅订阅 items.length,购物车列表变化时才会重渲染
  const itemCount = useCartStore(state => state.items.length)

  return <span className="cart-badge">购物车 ({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)}>加入购物车</button>
    </div>
  )
}

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

  if (items.length === 0) return <p>购物车是空的</p>

  return (
    <div>
      <h2>购物车</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)}>删除</button>
        </div>
      ))}
      <hr />
      <p><strong>合计:${total}</strong></p>
      <button onClick={clearCart}>清空购物车</button>
    </div>
  )
}
逻辑代码 82 行(超过 40 行限制,仅展示)

Zustand 的关键特性:

▶ 示例 2:Zustand 持久化中间件

Tom 希望用户刷新页面后购物车数据不丢失。Zustand 的 persist 中间件可以自动将 store 同步到 localStorage。

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

// persist 中间件包裹 store 定义
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 的 key
      // 默认存储所有字段,也可以 partialize 选择存储部分字段
      // partialize: (state) => ({ items: state.items }),
    }
  )
)

// 使用方式完全不变
function CartIcon() {
  const itemCount = useCartStore(state => state.items.length)
  return <span>购物车 ({itemCount})</span>
}

// localStorage 中存储的数据格式:
// key: 'cart-storage'
// value: '{"state":{"items":[...],"total":99},"version":0}'
▶ 试一试

持久化中间件的工作原理: 任何 state 变更都会自动同步到 localStorage,应用启动时自动从 localStorage 恢复数据。用户刷新页面后,购物车状态完全保留。

▶ 示例 3:Zustand DevTools 调试

在开发阶段,Tom 希望像 Redux DevTools 一样查看状态变更历史。Zustand 的 devtools 中间件可以无缝接入浏览器 DevTools 扩展。

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

// 同时使用多个中间件:用嵌套方式组合
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 中显示的 store 名称
  )
)

// 在浏览器中打开 Redux DevTools 扩展
// 可以看到 "AppStore" 标签下的所有 state 变更记录
// 支持时间旅行调试(跳转到任意历史状态)
▶ 试一试

Zustand 中间件系统的特点: 中间件通过函数嵌套组合,从外到内依次执行。devtools(persist(...)) 表示先连接 DevTools,再处理持久化。多个中间件可以灵活组合,满足不同需求。例如还可以加入 immer 中间件获得可变式语法,或自定义中间件记录每次 action 的日志。



4. Redux Toolkit:企业级方案

当项目规模增大到多个团队协作时,Zustand 的灵活性反而可能成为问题——每个开发者写 action 的风格不一致。Redux Toolkit 提供了标准化的状态管理流程,强制团队遵循统一模式。

BASH
npm install @reduxjs/toolkit react-redux

▶ 示例 4:Redux Toolkit 完整流程

JSX 📖 仅展示
import { createSlice, configureStore } from '@reduxjs/toolkit'
import { Provider, useSelector, useDispatch } from 'react-redux'

// ========== 1. 创建 Slice(定义 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 = []
    }
  }
})

// 自动生成的 action creators
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions

// ========== 2. 配置 Store ==========
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    // 以后可以添加更多 slice:
    // cart: cartSlice.reducer,
    // user: userSlice.reducer,
  }
})

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

// ========== 4. 组件中使用 ==========
function Counter() {
  // 选择性子读取
  const value = useSelector(state => state.counter.value)
  const history = useSelector(state => state.counter.history)
  const dispatch = useDispatch()

  return (
    <div>
      <h2>计数器:{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())}>重置</button>
      </div>
      <div>
        <h3>操作历史</h3>
        <ul>
          {history.slice(-5).map((h, i) => (
            <li key={i}>{h}</li>
          ))}
        </ul>
      </div>
    </div>
  )
}
逻辑代码 61 行(超过 40 行限制,仅展示)

Redux Toolkit 的关键概念:

概念 作用 类比
createSlice 创建一组相关的 state + reducers 文件抽屉:归类存放
configureStore 组合所有 slice 生成全局 store 文件柜:统一管理
Provider 将 store 注入 React 组件树 电源线:接通电源
useSelector 从 store 中读取状态片段 打开抽屉取文件
useDispatch 触发 action 更新状态 发出指令让管理员操作

▶ 示例 5:Redux Toolkit 异步操作

真实项目中的状态更新往往涉及异步逻辑(如登录、获取用户信息)。Redux Toolkit 内置了 createAsyncThunk 处理异步操作的生命周期(pending / fulfilled / rejected)。

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

// 1. 定义异步 thunk(自动生成 pending/fulfilled/rejected 三种 action)
const fetchUser = createAsyncThunk(
  'user/fetchUser',      // action 类型前缀
  async (userId, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/users/${userId}`)
      if (!response.ok) throw new Error('获取用户失败')
      return await response.json()
    } catch (err) {
      return rejectWithValue(err.message)
    }
  }
)

// 2. 创建 slice
const userSlice = createSlice({
  name: 'user',
  initialState: {
    data: null,
    loading: false,
    error: null
  },
  reducers: {
    clearUser: (state) => {
      state.data = null
      state.error = null
    }
  },
  // 处理异步 thunk 的三种状态
  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. 配置 store
const store = configureStore({
  reducer: { user: userSlice.reducer }
})

// 4. 组件中使用
function UserProfile({ userId }) {
  const dispatch = useDispatch()
  const { data, loading, error } = useSelector(state => state.user)

  return (
    <div>
      <button onClick={() => dispatch(fetchUser(userId))}>加载用户</button>
      {loading && <p>加载中...</p>}
      {error && <p>错误:{error}</p>}
      {data && <p>{data.name} — {data.email}</p>}
    </div>
  )
}
逻辑代码 59 行(超过 40 行限制,仅展示)

createAsyncThunk 自动处理了三态: 发起请求时 dispatch pending → 成功时 dispatch fulfilled(携带数据) → 失败时 dispatch rejected(携带错误信息)。开发者只需在 extraReducers 中定义每种状态下的 state 如何变化。与直接使用 useEffect + fetch 相比,这种模式的优势在于:请求状态自动追踪、action 可被 DevTools 记录、多个组件可以订阅同一个 thunk 的结果。



5. 选型对比

维度 Zustand Redux Toolkit
代码量 极少,一个 create() 函数搞定 中等,slice/store/Provider 全套
学习曲线 低(10 分钟上手) 中(需要理解 reducer/action/dispatch/thunk)
Provider 不需要 必须 Provider 包裹
选择性订阅 原生支持,选择器函数 useSelector + 浅比较
TypeScript 原生友好 良好,有类型推导
中间件 persist / devtools / immer 等 内置 thunk + 可扩展 saga/epic
异步处理 在 action 中自行实现 createAsyncThunk 内置
包大小 ~1KB ~12KB
适用场景 中小型应用、组件间共享状态 大型应用、复杂数据流、多团队协作
DevTools 支持(需中间件) 原生支持,时间旅行调试

选择建议:


❓ 常见问题

Q Zustand 和 Context + useReducer 怎么选?
A Zustand 性能更好——Context 更新时所有 Consumer 都会重渲染,而 Zustand 支持细粒度的选择性订阅。Zustand 代码更少,没有 Provider 嵌套。Context + useReducer 适合极其简单的场景(如主题切换),大多数情况下 Zustand 是更好的选择。
Q Redux Toolkit 比传统 Redux 简化了什么?
A 传统 Redux 需要手动编写 action type 常量、action creator、reducer switch-case,还要单独配置 Redux DevTools 和中间件。Redux Toolkit 的 createSlice 自动生成 action type 和 action creator,configureStore 自动整合中间件和 DevTools,createAsyncThunk 简化异步流程。代码量减少约 60%。
Q Zustand 的 Store 可以在组件外使用吗?
A 可以。useCartStore.getState() 在任何地方获取当前状态,useCartStore.setState(...) 在任何地方更新状态。这对于在路由守卫、拦截器、WebSocket 回调中访问状态非常有用。
Q 两个库可以同时用在一个项目中吗?
A 完全可以。很多项目前期用 Zustand,后期某个模块复杂度上升后引入 Redux Toolkit。两者不冲突——Zustand 管理简单的全局状态(主题、用户信息),Redux Toolkit 管理复杂业务模块(订单流程、审批流)。
Q 使用 useReducer 满足不了需求时才上状态管理库吗?
A useReducer 适合组件内部或局部的复杂状态逻辑。当状态需要在多个无关组件间共享、或者需要持久化、或者有中间件需求时,才需要引入 Zustand 或 Redux Toolkit。不要为了用而用。
Q Zustand 的 set 函数和 React 的 setState 有什么区别?
A Zustand 的 set 默认是浅合并(类似类组件的 setState),而 Redux Toolkit 的 reducer 基于 Immer 实现深层次的不可变更新。Zustand 也可以搭配 immer 中间件获得类似 Redux Toolkit 的可变式更新语法。两者在使用方式上各有优劣,根据团队习惯选择即可。

📖 小节


📝 作业

  1. 用 Zustand 创建一个 Todo Store:支持添加任务、切换完成状态、删除任务、清除已完成任务、统计未完成任务数量。在列表组件和统计组件中分别使用,验证选择性订阅的效果。
  2. 给 Todo Store 添加 persist 中间件,验证刷新页面后数据是否保留。
  3. 用 Redux Toolkit 实现一个用户管理模块:用 createAsyncThunk 模拟获取用户列表(延迟 1 秒返回数据),管理 loading / error / data 三态,组件中显示加载态、错误态和数据列表。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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