状态管理 —— 从Context到Zustand
当你的应用只有三五个组件时,用 props 传数据毫不费力。但当组件树长到几十层深、十几个组件需要共享同一份用户信息或购物车数据时,你会发现自己在做一件荒唐的事——把 props 像接力棒一样层层传递,途经的中间组件根本不关心这些数据,却不得不当个”快递中转站”。这就是状态管理要解决的核心问题。本章将从 React 内置方案出发,一路走到当下最流行的 Zustand,帮你找到”刚好够用”的那把钥匙。
📋 开篇自测:你已经知道多少?
- 你能解释 props drilling 的含义,并说出它带来的两个以上具体问题吗?
- Context 配合 useReducer 已经能做状态管理了,为什么社区还是涌现了 Redux、Zustand、Jotai 这些第三方库?
- 如果让你为一个新项目选择状态管理方案,你的选型标准是什么?
一、为什么需要状态管理?—— props drilling 的真实痛感
1.1 一个让人抓狂的场景
假设你正在做一个电商应用,用户登录后的信息(用户名、头像、会员等级)需要在以下地方使用:顶部导航栏、侧边栏、商品详情页的”专属价格”、购物车页面、个人中心。这些组件散落在组件树的各个角落。
如果只用 props 传递,组件树大致像这样:
function App() {
const [user, setUser] = useState({ name: '小明', level: 'vip' });
return (
<Layout user={user}> {/* Layout 自己不用 user */}
<Header user={user}> {/* Header 只用了 user.name */}
<NavBar user={user}> {/* NavBar 不用 user,但要往下传 */}
<UserAvatar user={user} /> {/* 终于用到了 */}
</NavBar>
</Header>
<Content user={user}> {/* Content 不用 user */}
<ProductPage user={user}> {/* ProductPage 不用 user */}
<PriceTag user={user} /> {/* 终于用到了 user.level */}
</ProductPage>
</Content>
</Layout>
);
}
这就是 props drilling(属性穿透)——数据像打井一样,必须穿过一层又一层不相干的岩层才能到达目标。
1.2 Props drilling 的三大痛点
痛点一:中间组件被”污染”。 Layout、Content、NavBar 这些组件本来和 user 毫无关系,却被迫接受并传递一个自己用不上的 prop。一旦 user 的结构发生变化(比如新增一个 avatar 字段),所有中间组件的类型定义都要跟着改。
痛点二:重构成本高。 如果你想把 UserAvatar 从 NavBar 移到 Sidebar 下面,整条 props 传递链都得重新布线。
痛点三:不必要的重渲染。 当 user 变化时,整条传递链上的所有组件都会重新渲染——即使它们只是”路过”而已。
打一个比方:props drilling 就像快递必须从北京总仓发到省仓、市仓、区仓、站点,最后才到你手里。如果每个包裹都要走这条链路,效率极低。而状态管理就像京东的前置仓——货直接放在离你最近的地方,需要的人直接去取。
🤔 想一想 如果一个应用只有 2-3 层组件嵌套,你还会选择引入状态管理库吗?过度设计和设计不足,你更害怕哪个?
二、Context + useReducer —— React 内置的”够用”方案
在引入第三方库之前,先看看 React 自己能做到什么程度。
2.1 Context:跨层传递数据
createContext + useContext 可以让数据跳过中间组件直达目标:
import { createContext, useContext, useState } from 'react';
// 1. 创建 Context
const UserContext = createContext(null);
// 2. 在顶层提供数据
function App() {
const [user, setUser] = useState({ name: '小明', level: 'vip' });
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout>
<Header>
<NavBar>
<UserAvatar /> {/* 不需要 props 了 */}
</NavBar>
</Header>
</Layout>
</UserContext.Provider>
);
}
// 3. 在任意深度消费数据
function UserAvatar() {
const { user } = useContext(UserContext);
return <span>你好,{user.name}</span>;
}
中间的 Layout、Header、NavBar 完全不用感知 user 的存在。这就是 Context 的核心价值——穿透组件层级,直达消费者。
2.2 配合 useReducer 管理复杂状态
当状态逻辑变复杂(多个 action、状态之间有依赖),useState 就捉襟见肘了。useReducer 提供了类似 Redux 的 dispatch/action 模式:
import { createContext, useContext, useReducer } from 'react';
// 定义 reducer
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'RESET':
return { ...state, count: 0 };
default:
throw new Error(`未知 action: ${action.type}`);
}
}
// 创建 Context
const CounterContext = createContext(null);
function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
// 消费者组件
function Counter() {
const { state, dispatch } = useContext(CounterContext);
return (
<div>
<h2>计数:{state.count}</h2>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
</div>
);
}
function App() {
return (
<CounterProvider>
<Counter />
</CounterProvider>
);
}
2.3 Context 的性能痛点
Context 方案看起来不错,但有一个关键缺陷:Provider 的 value 变化时,所有消费者都会重新渲染,无论它们是否用到了变化的那部分数据。
const AppContext = createContext(null);
function AppProvider({ children }) {
const [user, setUser] = useState({ name: '小明' });
const [theme, setTheme] = useState('light');
// user 和 theme 放在同一个 Context 里
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AppContext.Provider>
);
}
// 这个组件只用了 theme
function ThemeToggle() {
const { theme, setTheme } = useContext(AppContext);
console.log('ThemeToggle 渲染了'); // user 变化时也会打印!
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
当前主题:{theme}
</button>
);
}
当 user 更新时,ThemeToggle 明明只用了 theme,却也被迫重新渲染。这在小项目中无所谓,但当消费者组件变多、渲染开销变大时,就会成为性能瓶颈。
常见的缓解方式: 把不同领域的状态拆到不同的 Context 里(一个 UserContext、一个 ThemeContext)。但如果状态之间有交叉依赖,拆分就变得不现实了。
小结: Context + useReducer 适合中小型应用或组件库内部的状态共享。当你发现自己在拆分 Context、手动做 memo 优化、状态之间耦合严重时,就是时候考虑第三方方案了。
三、Redux Toolkit —— 经典方案的现代写法
Redux 是 React 生态中最早也最知名的状态管理库。早期的 Redux 以”写法繁琐”闻名——定义一个功能需要分别写 actionType、actionCreator、reducer 三个文件。Redux Toolkit(RTK)是 Redux 官方推出的”最佳实践”封装,大幅减少了样板代码。
3.1 用 Redux Toolkit 实现计数器
先安装依赖:
npm install @reduxjs/toolkit react-redux
用 createSlice 同时定义 state、reducers 和 actions:
// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment(state) {
state.count += 1; // RTK 内部用 Immer,可以直接"修改"状态(注意:只有在 RTK 的 createSlice 中才能这样写,普通 Redux reducer 不行)
},
decrement(state) {
state.count -= 1;
},
reset(state) {
state.count = 0;
},
},
});
export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;
用 configureStore 创建 store:
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
在组件中使用:
// App.jsx
import { Provider, useSelector, useDispatch } from 'react-redux';
import { store } from './store';
import { increment, decrement, reset } from './store/counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();
return (
<div>
<h2>计数:{count}</h2>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
<button onClick={() => dispatch(reset())}>重置</button>
</div>
);
}
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
3.2 RTK 的优势与代价
优势:
- 内置 Immer,可以用”看起来可变”的语法写不可变更新
- 内置 DevTools 支持,时间旅行调试
- 拥有庞大的生态系统(RTK Query 处理异步请求、中间件系统)
- 适合大团队——严格的单向数据流,约束明确
代价:
- 概念较多:store、slice、action、dispatch、selector
- 即使有了 RTK,仍然需要 Provider 包裹
- 对于简单场景来说仍然”太重”——一个计数器需要 3 个文件
🤔 想一想 Redux 的核心设计哲学是”可预测的状态变更”——所有状态修改必须通过 dispatch action 触发,这像不像银行转账必须走柜台流程?这种约束在什么规模的项目中才真正有价值?
四、Zustand —— 极简状态管理
Zustand(德语”状态”的意思)是近几年增长最快的 React 状态管理库。它的设计哲学可以用一句话概括:能少写一行代码就少写一行。
4.1 用 Zustand 实现同样的计数器
安装:
npm install zustand
定义 store:
// store/useCounterStore.js
import { create } from 'zustand';
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
export default useCounterStore;
在组件中使用:
// App.jsx
import useCounterStore from './store/useCounterStore';
function Counter() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
const reset = useCounterStore((state) => state.reset);
return (
<div>
<h2>计数:{count}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>重置</button>
</div>
);
}
function App() {
return <Counter />; // 不需要 Provider!
}
对比一下代码量:Context 方案大约 40 行,Redux Toolkit 大约 35 行(分散在 3 个文件),Zustand 大约 15 行(1 个文件)。
4.2 Zustand 为什么增长这么快?
第一,不需要 Provider。 Zustand 的 store 是一个独立于 React 组件树的外部存储。这意味着你可以在任何地方(包括 React 组件外部的工具函数里)读写状态,不受组件层级限制。
// 甚至可以在组件外部读写状态
const currentCount = useCounterStore.getState().count;
useCounterStore.getState().increment();
第二,精准的重渲染控制。 使用选择器(selector)只订阅需要的部分状态,其他状态变化不会触发组件重渲染:
// 只订阅 count,increment 变化不会导致重渲染
const count = useCounterStore((state) => state.count);
第三,几乎零概念。 没有 action type、没有 dispatch、没有 reducer,store 就是一个包含状态和方法的普通对象。
第四,体积极小。 Zustand gzip 后仅约 1.2KB,而 Redux Toolkit + react-redux 加在一起 tree-shaken 后约 11-14KB,体积差距显著。
4.3 Zustand 的中间件
Zustand 虽然简单,但并不简陋。它提供了强大的中间件系统:
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useCounterStore = create(
devtools(
persist(
(set) => ({
count: 0,
increment: () => set(
(state) => ({ count: state.count + 1 }),
false,
'increment' // devtools 中显示的 action 名称
),
}),
{ name: 'counter-storage' } // localStorage 的 key
)
)
);
devtools:接入 Redux DevTools 浏览器扩展,支持时间旅行调试persist:自动将状态持久化到 localStorage,页面刷新后状态不丢失immer:允许用可变语法写不可变更新(和 RTK 类似)
五、Jotai —— 原子化状态管理
如果说 Zustand 是”把整个仓库打包成一个 store”,那么 Jotai 的思路完全相反——把状态拆成一个个最小的原子(atom),按需组合。
5.1 用 Jotai 实现计数器
安装:
npm install jotai
import { atom, useAtom } from 'jotai';
// 定义原子状态
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<h2>计数:{count}</h2>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={() => setCount((c) => c - 1)}>-1</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
);
}
5.2 原子化的威力:派生状态
Jotai 真正的亮点在于原子的组合——你可以从已有的原子派生出新的原子:
const countAtom = atom(0);
// 派生只读原子:自动跟踪 countAtom 的变化
const doubledAtom = atom((get) => get(countAtom) * 2);
// 派生读写原子
const countWithLimitAtom = atom(
(get) => get(countAtom),
(get, set, newValue) => {
// 限制 count 不超过 100
set(countAtom, Math.min(newValue, 100));
}
);
function Display() {
const [doubled] = useAtom(doubledAtom);
return <p>双倍值:{doubled}</p>; // 自动响应 countAtom 的变化
}
原子化的优势: 每个组件只订阅自己真正依赖的原子,状态变化时只有相关组件重渲染,天然避免了 Context 的”全量通知”问题。
适合场景: 状态之间有复杂的派生关系,或者你希望状态的定义和组件解耦、可以在任意地方自由组合。
六、方案选型指南 —— 什么项目用什么方案
说了这么多方案,到底该怎么选?先看一张对比表:
| 维度 | Context + useReducer | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|---|
| 额外依赖 | 无(React 内置) | @reduxjs/toolkit + react-redux | zustand | jotai |
| gzip 体积 | 0 KB | ~11-14 KB | ~1 KB | ~4 KB |
| 学习成本 | 低 | 中高 | 极低 | 低 |
| 需要 Provider | 是 | 是 | 否 | 可选 |
| DevTools | 无 | 内置 | 中间件支持 | 官方扩展 |
| 异步处理 | 需要手动处理 | RTK Query | 直接在 action 中写 | 支持异步 atom |
| 精准重渲染 | 差(整个 Context 消费者都更新) | 好(useSelector) | 好(selector) | 好(原子级别) |
| TypeScript 支持 | 需要手动定义类型 | 自动推断 | 自动推断 | 自动推断 |
| 组件外访问 | 不支持 | store.getState() | store.getState() | 需要自建 store |
6.1 选型决策树
按照以下思路选择,通常不会出错:
小型项目(个人博客、工具页、5 个以下共享状态): Context + useReducer 足够了,无需引入额外依赖。
中型项目(后台管理系统、电商前端、团队 3-5 人): Zustand 是当下的最优选。API 简单、体积小、性能好,上手几乎零成本。
大型项目(企业级复杂应用、团队 10+人、需要严格的代码规范): Redux Toolkit 仍然是稳妥选择。它的约束性(必须通过 action 修改状态)在大团队中反而是优势——代码可追溯、可审计。
状态关系复杂(大量派生状态、类似电子表格的联动计算): Jotai 的原子化模型天然适合这种场景。
🤔 想一想 你目前正在做或将要做的项目,大致属于上面哪个规模?如果让你现在选一个方案,你会选哪个?试着说出你的理由。
七、实战:用 Zustand 构建购物车状态管理
纸上得来终觉浅。下面我们用 Zustand 实现一个功能完整的购物车——包含添加商品、修改数量、删除商品、计算总价。
7.1 定义购物车 Store
// store/useCartStore.js
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useCartStore = create(
devtools(
persist(
(set, get) => ({
// 状态
items: [],
// items 的结构:[{ id, name, price, quantity }]
// 添加商品
addItem: (product) => set((state) => {
const existing = state.items.find((item) => item.id === product.id);
if (existing) {
// 已存在,数量 +1
return {
items: state.items.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
}
// 不存在,新增
return { items: [...state.items, { ...product, quantity: 1 }] };
}),
// 修改数量
updateQuantity: (id, quantity) => set((state) => {
if (quantity <= 0) {
// 数量为 0 则移除
return { items: state.items.filter((item) => item.id !== id) };
}
return {
items: state.items.map((item) =>
item.id === id ? { ...item, quantity } : item
),
};
}),
// 删除商品
removeItem: (id) => set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
// 清空购物车
clearCart: () => set({ items: [] }),
// 计算总价(使用 get 读取当前状态)
getTotalPrice: () => {
const { items } = get();
return items.reduce(
(total, item) => total + item.price * item.quantity,
0
);
},
// 计算总数量
getTotalCount: () => {
const { items } = get();
return items.reduce((total, item) => total + item.quantity, 0);
},
}),
{ name: 'shopping-cart' } // 持久化到 localStorage
),
{ name: 'CartStore' } // DevTools 中显示的名称
)
);
export default useCartStore;
7.2 商品列表组件
// components/ProductList.jsx
import useCartStore from '../store/useCartStore';
const PRODUCTS = [
{ id: 1, name: 'React 实战教程', price: 99 },
{ id: 2, name: 'TypeScript 入门', price: 79 },
{ id: 3, name: 'Node.js 进阶', price: 89 },
];
function ProductList() {
const addItem = useCartStore((state) => state.addItem);
return (
<div>
<h2>商品列表</h2>
{PRODUCTS.map((product) => (
<div key={product.id} style={{ display: 'flex', gap: 12, marginBottom: 8 }}>
<span>{product.name}</span>
<span>¥{product.price}</span>
<button onClick={() => addItem(product)}>加入购物车</button>
</div>
))}
</div>
);
}
export default ProductList;
7.3 购物车组件
// components/Cart.jsx
import useCartStore from '../store/useCartStore';
function Cart() {
const items = useCartStore((state) => state.items);
const updateQuantity = useCartStore((state) => state.updateQuantity);
const removeItem = useCartStore((state) => state.removeItem);
const clearCart = useCartStore((state) => state.clearCart);
const getTotalPrice = useCartStore((state) => state.getTotalPrice);
const getTotalCount = useCartStore((state) => state.getTotalCount);
if (items.length === 0) {
return <p>购物车是空的,去选购一些课程吧!</p>;
}
return (
<div>
<h2>购物车({getTotalCount()} 件)</h2>
{items.map((item) => (
<div key={item.id} style={{ display: 'flex', gap: 12, marginBottom: 8 }}>
<span>{item.name}</span>
<span>¥{item.price}</span>
<div>
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
<span style={{ margin: '0 8px' }}>{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
</div>
<button onClick={() => removeItem(item.id)}>删除</button>
</div>
))}
<hr />
<p><strong>总计:¥{getTotalPrice()}</strong></p>
<button onClick={clearCart}>清空购物车</button>
</div>
);
}
export default Cart;
7.4 导航栏中的购物车角标
Zustand 的精准订阅在这里体现得淋漓尽致——导航栏只关心商品总数,不关心具体商品信息。当用户修改某个商品的名称或价格时,导航栏不会重渲染。
// components/CartBadge.jsx
import useCartStore from '../store/useCartStore';
function CartBadge() {
// 直接在 selector 中计算派生值,Zustand 会在值变化时触发重渲染
const totalCount = useCartStore((state) =>
state.items.reduce((total, item) => total + item.quantity, 0)
);
return (
<span style={{
background: totalCount > 0 ? '#ff4d4f' : '#ccc',
color: '#fff',
borderRadius: '50%',
padding: '2px 8px',
fontSize: 12,
}}>
{totalCount}
</span>
);
}
export default CartBadge;
7.5 组装完整应用
// App.jsx
import ProductList from './components/ProductList';
import Cart from './components/Cart';
import CartBadge from './components/CartBadge';
function App() {
return (
<div style={{ padding: 24 }}>
<nav style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 24 }}>
<h1>课程商城</h1>
<div>购物车 <CartBadge /></div>
</nav>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }}>
<ProductList />
<Cart />
</div>
</div>
);
}
export default App;
注意整个应用中没有任何 Provider 包裹。ProductList 和 Cart 完全解耦——它们各自通过 useCartStore 读写同一份状态,彼此不需要知道对方的存在。这就是 Zustand “去中心化”的魅力。
7.6 在组件外部访问状态
购物车还有一个常见需求:在发起支付请求时(通常在一个工具函数里),需要读取购物车数据。Zustand 可以在组件外直接访问:
// utils/checkout.js
import useCartStore from '../store/useCartStore';
export async function checkout() {
const { items, getTotalPrice, clearCart } = useCartStore.getState();
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items,
totalPrice: getTotalPrice(),
}),
});
if (response.ok) {
clearCart(); // 支付成功,清空购物车
return true;
}
return false;
}
如果你用的是 Context 方案,这段代码根本无法实现——因为 Context 只能在 React 组件内部通过 useContext 访问。这也是 Zustand 在工程实践中的一大优势。
📝 掌握度自测
-
以下哪个不是 props drilling 带来的问题?
- A) 中间组件被迫接受不相关的 props
- B) 重构组件位置时需要重新布线 props 传递链
- C) 组件的 state 被全局共享导致安全问题
- D) 不必要的中间组件重渲染
-
React Context 方案的主要性能缺陷是什么?
- A) Context 的 value 无法传递函数
- B) Provider 的 value 变化时,所有消费者组件都会重渲染
- C) Context 不支持 TypeScript
- D) Context 不能嵌套使用
-
以下关于 Zustand 的描述,哪个是错误的?
- A) 不需要 Provider 包裹组件树
- B) 可以在 React 组件外部读写状态
- C) 必须配合 Redux DevTools 才能使用
- D) gzip 后体积约 1KB
-
Jotai 的”原子化”状态管理,其核心优势是什么?
- A) 比 Zustand 体积更小
- B) 状态按原子粒度订阅,天然避免不必要的重渲染
- C) 不需要安装任何依赖
- D) 只支持同步状态
-
在一个 10 人团队开发的企业级复杂后台系统中,以下哪个选择最合理?
- A) 只用 useState,坚决不引入状态管理
- B) 用 Context + useReducer,保持零依赖
- C) 用 Redux Toolkit,利用其约束性保证代码规范
- D) 用 Jotai,因为它最新最潮
💡 自我评估
- 答对 5 题:你已经对 React 状态管理的全局图景有了清晰的认识,可以根据项目需求做出合理的技术选型。
- 答对 3-4 题:基础不错,建议回顾 Context 的性能缺陷和各方案的适用场景,这是面试高频考点。
- 答对 0-2 题:建议把文中的计数器示例分别用 Context、Redux Toolkit、Zustand 实现一遍,动手写过才能真正理解差异。
参考答案: 1-C, 2-B, 3-C, 4-B, 5-C
购买课程解锁全部内容
从组件到架构:12 章系统掌握现代 React
¥29.90