React: 状态管理:Zustand / Redux
最后更新:2026-08-26
Tom 的购物车功能最初用
useState加上props逐层传递,勉强能用。但随着组件树越来越深(App → Header → CartIcon,App → ProductList → ProductCard → AddToCartButton),修改购物车状态需要在 5 层组件间传递回调函数,稍有不慎就会漏传或错传。他意识到:需要一套全局状态管理方案,让任意组件都能直接读写共享状态。
1. 你将学到
- Zustand 极简状态管理的核心 API
- Redux Toolkit 的完整工作流(Slice → Store → Provider → Hooks)
- 两种方案的选型依据和适用场景
- 持久化存储中间件的使用
- TypeScript 下的类型安全状态管理
2. 概念图解
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 逐层传递,但它有两个严重问题:
- 性能问题: Context 更新时,所有消费该 Context 的组件都会重渲染,即使它们只读不写。购物车场景中,修改数量时购物车图标也会重渲染。
- 无内置 action 机制: Context 只提供值的传递,不提供状态更新的规范模式,容易写出散落各处的更新逻辑。
专门的状态管理库(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 包裹。
npm install zustand
▶ 示例 1:Zustand 购物车 Store
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>
)
}
Zustand 的关键特性:
- 零 Provider: 不需要包裹
<Provider>,直接在任意组件中调用 Hook - 选择性子订阅:
useCartStore(state => state.items.length)只在items.length变化时触发重渲染,性能优于 Context - set + get:
set更新状态,get读取当前状态(适合在 action 中读取其他 state)
▶ 示例 2:Zustand 持久化中间件
Tom 希望用户刷新页面后购物车数据不丢失。Zustand 的 persist 中间件可以自动将 store 同步到 localStorage。
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 扩展。
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 提供了标准化的状态管理流程,强制团队遵循统一模式。
npm install @reduxjs/toolkit react-redux
▶ 示例 4:Redux Toolkit 完整流程
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>
)
}
Redux Toolkit 的关键概念:
| 概念 | 作用 | 类比 |
|---|---|---|
createSlice |
创建一组相关的 state + reducers | 文件抽屉:归类存放 |
configureStore |
组合所有 slice 生成全局 store | 文件柜:统一管理 |
Provider |
将 store 注入 React 组件树 | 电源线:接通电源 |
useSelector |
从 store 中读取状态片段 | 打开抽屉取文件 |
useDispatch |
触发 action 更新状态 | 发出指令让管理员操作 |
▶ 示例 5:Redux Toolkit 异步操作
真实项目中的状态更新往往涉及异步逻辑(如登录、获取用户信息)。Redux Toolkit 内置了 createAsyncThunk 处理异步操作的生命周期(pending / fulfilled / rejected)。
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>
)
}
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 | 支持(需中间件) | 原生支持,时间旅行调试 |
选择建议:
- 小项目 / 原型 / 个人项目 → Zustand。代码少、心智负担低、够用。
- 大项目 / 多团队 / 需要严格规范 → Redux Toolkit。标准化流程让不同团队的代码风格一致。
- 两者混合使用也是常见模式:跨页面共享的全局状态用 Zustand,复杂业务模块用 Redux Toolkit。
❓ 常见问题
useCartStore.getState() 在任何地方获取当前状态,useCartStore.setState(...) 在任何地方更新状态。这对于在路由守卫、拦截器、WebSocket 回调中访问状态非常有用。set 默认是浅合并(类似类组件的 setState),而 Redux Toolkit 的 reducer 基于 Immer 实现深层次的不可变更新。Zustand 也可以搭配 immer 中间件获得类似 Redux Toolkit 的可变式更新语法。两者在使用方式上各有优劣,根据团队习惯选择即可。📖 小节
- Zustand 以极简 API 著称:
create(set => ({ state, actions })),零 Provider,选择性子订阅 - 持久化中间件
persist一行代码实现状态自动同步到 localStorage - devtools 中间件将 Zustand 接入 Redux DevTools,支持时间旅行调试
- Redux Toolkit 提供标准化流程:createSlice → configureStore → Provider → useSelector/useDispatch
- createAsyncThunk 自动管理异步操作的三种状态(pending/fulfilled/rejected)
- 小型项目推荐 Zustand,大型项目推荐 Redux Toolkit,两者可混合使用
- 中间件系统(Zustand 的函数嵌套 vs Redux Toolkit 的 builder 模式)提供灵活的扩展能力
- Zustand 适合 80% 的共享状态场景,Redux Toolkit 适合需要严格规范的复杂业务模块
- 无论选哪个,建议在项目初期就确定状态管理方案,避免中途切换带来的迁移成本
📝 作业
- 用 Zustand 创建一个 Todo Store:支持添加任务、切换完成状态、删除任务、清除已完成任务、统计未完成任务数量。在列表组件和统计组件中分别使用,验证选择性订阅的效果。
- 给 Todo Store 添加
persist中间件,验证刷新页面后数据是否保留。 - 用 Redux Toolkit 实现一个用户管理模块:用 createAsyncThunk 模拟获取用户列表(延迟 1 秒返回数据),管理 loading / error / data 三态,组件中显示加载态、错误态和数据列表。