你有没有遇到过这种情况:项目里用着 Context API 管理状态,结果随着业务复杂度上升,组件重渲染越来越频繁,性能开始拉胯。然后你开始怀疑:是不是该换个状态管理方案了?
今天咱们就把话说清楚:Context API 和 Zustand 到底该怎么选,什么时候该用谁。
先说结论:它们根本不是竞争关系
很多人把 Context API 和 Zustand 放在一起对比,好像它们是二选一的关系。其实不是。
Context API 是 React 内置的数据传递机制,设计初衷是解决 props drilling(属性透传)问题。你可以把它理解为一个"数据管道",让深层组件不用通过中间组件一层层传 props。
Zustand 是一个专门的状态管理库,设计目标是提供高性能、低心智负担的全局状态解决方案。它不仅能传递数据,还内置了性能优化和强大的状态操作能力。
所以准确的说法是:Context API 是基础设施 ,Zustand 是专业工具。就像你可以用螺丝刀拧螺丝,但电钻会更高效。
代码对比:一眼看出区别
Context API 的标准写法
tsx
// 第一步:创建 Context
const TokenContext = createContext(undefined);
// 第二步:写个 Provider 组件
export function TokenProvider({ children }) {
const [count, setCount] = useState(0);
return (
<TokenContext.Provider value={{ count, setCount }}>
{children}
</TokenContext.Provider>
);
}
// 第三步:封装个 Hook
export function useToken() {
const context = useContext(TokenContext);
if (!context) throw new Error("必须在 Provider 内使用");
return context;
}
// 第四步:在 App 里包裹 Provider
<TokenProvider>
<App />
</TokenProvider>
// 第五步:终于能用了
function MyComponent() {
const { count, setCount } = useToken();
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
你看这个流程:创建 Context → 写 Provider → 封装 Hook → 包裹组件 → 使用。五步才能跑起来,代码散落在多个地方。
Zustand 的写法
tsx
// 一步到位:创建 store
import { create } from 'zustand';
export const useTokenStore = create((set) => ({
count: 0,
setCount: (count) => set({ count }),
}));
// 直接用,没了
function MyComponent() {
const { count, setCount } = useTokenStore();
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
注意看,没有 Provider,不需要包裹任何东西。创建 store → 使用,就这么简单。
这不是语法糖的区别,而是设计理念的不同。Context API 需要你手动搭建整个数据流,Zustand 直接给你一个开箱即用的状态容器。
性能差距:这才是关键
很多人觉得 Context 够用了,直到遇到性能问题。
Context API 的性能陷阱
tsx
const TokenProvider = ({ children }) => {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<TokenContext.Provider value={{ count, setCount, name, setName }}>
{children}
</TokenContext.Provider>
);
};
function Counter() {
const { count } = useToken();
console.log('Counter 重渲染了');
return <div>{count}</div>;
}
function UserName() {
const { name } = useToken();
console.log('UserName 重渲染了');
return <div>{name}</div>;
}
问题来了:当你调用 setName 修改名字时,Counter 组件会重渲染吗?
会。
尽管 Counter 只用了 count,但只要 Provider 的 value 对象发生任何变化,所有消费该 Context 的组件都会重新渲染。这是 Context API 的工作机制决定的------它无法做到精确订阅。
你可以用 useMemo 优化 Provider 的 value,或者拆分成多个 Context,但这会让代码变得更复杂。这就是 Context API 在处理复杂状态时的本质问题:粒度太粗。
Zustand 的精确订阅
tsx
const useStore = create((set) => ({
count: 0,
name: '',
setCount: (count) => set({ count }),
setName: (name) => set({ name }),
}));
function Counter() {
const count = useStore((state) => state.count);
console.log('Counter 重渲染了');
return <div>{count}</div>;
}
function UserName() {
const name = useStore((state) => state.name);
console.log('UserName 重渲染了');
return <div>{name}</div>;
}
现在调用 setName 修改名字,Counter 还会重渲染吗?
不会。
Zustand 通过 selector(选择器)实现精确订阅。Counter 只订阅了 count,name 的变化不会触发它的重渲染。这不是什么黑科技,就是状态管理库该有的基本能力。
使用场景:别什么都往一个筐里装
Context API 适合的场景
Context API 不是不好,而是要用对地方。
主题切换:
tsx
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<MyApp />
</ThemeContext.Provider>
);
}
主题数据有什么特点?简单 、不常变 、逻辑少。用户不会每秒切换十次主题,整个应用的主题状态就是一个字符串加一个 setter。这种场景下 Context API 完全够用,引入 Zustand 反而是过度设计。
用户信息:
tsx
const UserContext = createContext();
用户登录后,用户信息基本不变。偶尔更新个头像、修改个昵称,频率很低。这种配置型数据用 Context 管理,简洁明了。
语言设置:
tsx
const I18nContext = createContext();
国际化配置同理,切换语言是低频操作,不需要复杂的状态管理。
总结: Context API 适合配置型数据,特点是更新频率低、逻辑简单、状态扁平。
Zustand 适合的场景
购物车:
tsx
const useCartStore = create((set) => ({
items: [],
total: 0,
addItem: (item) => set((state) => ({
items: [...state.items, item],
total: state.total + item.price
})),
removeItem: (id) => set((state) => {
const newItems = state.items.filter(i => i.id !== id);
const removedItem = state.items.find(i => i.id === id);
return {
items: newItems,
total: state.total - removedItem.price
};
}),
clearCart: () => set({ items: [], total: 0 }),
}));
购物车是什么状态?频繁增删改 、有业务逻辑 、多字段联动。用户可能疯狂加购、删除、修改数量,每次操作都要同步更新 total、items、可能还有优惠券计算。这种复杂度用 Context 管理,代码会变得非常臃肿。
Token 统计(实时更新):
tsx
const useTokenStore = create((set) => ({
tokenData: null,
loading: false,
updateTokens: (newMessage) => set((state) => {
if (!state.tokenData) return state;
const tokenIncrease = newMessage.token_counts || 0;
const newBreakdown = { ...state.tokenData.token_breakdown };
if (newMessage.sender === 'ai') {
newBreakdown.ai_responses += tokenIncrease;
} else if (newMessage.sender === 'agent') {
newBreakdown.agent_responses += tokenIncrease;
}
return {
tokenData: {
...state.tokenData,
total_tokens: state.tokenData.total_tokens + tokenIncrease,
token_breakdown: newBreakdown,
message_count: state.tokenData.message_count + 1,
last_updated: new Date().toISOString(),
}
};
}),
refreshTokens: async (sessionId) => {
set({ loading: true });
const response = await fetchTokens(sessionId);
set({ tokenData: response.data, loading: false });
}
}));
Token 统计需要:每次消息发送后立即更新、区分不同类型的 token、计算总量、保存时间戳。这是典型的业务状态,逻辑复杂、更新频繁、需要跨多个组件访问(Header 显示总量、侧边栏显示详情、聊天面板触发更新)。
WebSocket 消息流:
tsx
const useMessageStore = create((set) => ({
messages: [],
connected: false,
addMessage: (msg) => set((state) => ({
messages: [...state.messages, { ...msg, id: Date.now() }]
})),
setConnected: (status) => set({ connected: status }),
}));
WebSocket 每秒可能接收多条消息,高频更新。用 Context 的话,每次新消息都会触发所有消费组件重渲染,性能灾难。
表单状态(多字段联动):
tsx
const useFormStore = create((set) => ({
email: '',
password: '',
confirmPassword: '',
errors: {},
setEmail: (email) => set({ email, errors: {} }),
setPassword: (password) => set((state) => {
const errors = { ...state.errors };
if (password.length < 8) {
errors.password = '密码至少8位';
} else {
delete errors.password;
}
return { password, errors };
}),
validate: () => {
// 复杂的表单验证逻辑
}
}));
表单验证涉及字段联动、实时校验、错误提示,逻辑比较重。Zustand 可以把所有逻辑封装在 store 里,组件只负责展示。
总结: Zustand 适合业务数据,特点是更新频繁、逻辑复杂、需要跨组件操作、可能需要在组件外调用。
组件外调用:Context 做不到的事
这是个很容易被忽略但非常实用的特性。
Context API 的限制
tsx
// ❌ 这样写会报错
import { useToken } from './token-context';
export async function handleNewMessage(message) {
const { updateTokens } = useToken(); // 错误!Hook 只能在组件内调用
updateTokens(message);
}
Context API 基于 React 的 Hook 系统,必须在组件内部调用。如果你想在工具函数、API 请求、事件监听器里更新状态,做不到。
你可能会说:"那我把函数传进组件里调用不就行了?"可以,但这会让代码变得很别扭:
tsx
function SomeComponent() {
const { updateTokens } = useToken();
useEffect(() => {
// 把 updateTokens 传给外部函数
setupWebSocket(updateTokens);
}, [updateTokens]);
}
这种写法不仅丑陋,还容易出现依赖追踪问题。
Zustand 的灵活性
tsx
// ✅ 随便调
import { useTokenStore } from './token-store';
export async function handleNewMessage(message) {
useTokenStore.getState().updateTokens(message);
}
// 在 WebSocket 回调里
socket.on('message', (data) => {
useTokenStore.getState().addMessage(data);
});
// 在 API 中间件里
axios.interceptors.response.use(response => {
if (response.headers['x-token-count']) {
useTokenStore.getState().updateTokens({
token_counts: parseInt(response.headers['x-token-count'])
});
}
return response;
});
Zustand 的 store 是一个普通的 JavaScript 对象,通过 getState() 可以在任何地方访问和修改状态。这在处理异步逻辑、第三方库集成时非常有用。
迁移成本:从 Context 到 Zustand
如果你现在项目里用的是 Context,想换成 Zustand,成本大不大?
不大。 核心逻辑几乎可以直接复制粘贴。
决策树:5 秒钟做出正确选择
markdown
你的状态需要跨组件共享吗?
├─ 不需要 → 用 useState(组件内状态)
└─ 需要 ↓
这个状态每分钟会更新超过 5 次吗?
├─ 不会 → Context API
│ (主题、语言、用户信息)
└─ 会 ↓
这个状态有复杂的计算逻辑或副作用吗?
├─ 没有 → Context API 勉强可以
└─ 有 → Zustand
(购物车、消息列表、Token 统计)
更简单的判断标准:
- 配置数据(主题、语言、用户信息)→ Context API
- 业务数据(购物车、消息、统计)→ Zustand
- 拿不准 → 先用 Context,发现性能问题再换 Zustand
常见误区
误区一:"Context API 够用就不要引入新依赖"
这话没错,但要看"够用"的定义。如果你的应用只有几个页面,状态简单,那确实够用。但如果你已经开始用 useMemo 优化 Provider、拆分多个 Context、为性能问题头疼,那就是"不够用"的信号。
误区二:"Zustand 是大型项目才需要的"
恰恰相反。Zustand 的设计目标就是降低心智负担,小项目用起来比 Context 还简单。你不需要理解 Provider、不需要担心重渲染、不需要写一堆模板代码。
真正的问题是:很多人习惯了 Context 的写法,懒得换。
误区三:"用了 Zustand 就不能用 Context"
它们可以共存。实际项目中经常是:
- Context 管理主题、语言等配置
- Zustand 管理购物车、消息等业务状态
混用示例:
tsx
// Context 管理主题
<ThemeProvider>
<App />
</ThemeProvider>
// Zustand 管理业务
const cart = useCartStore();
const messages = useMessageStore();
没有冲突,各司其职。
最后说两句
技术选型没有绝对的对错,只有合不合适。
Context API 是 React 生态的基础设施,它的存在有其合理性。但当你的应用状态管理变得复杂时,专业的工具会让你的工作轻松很多。
Zustand 解决的核心问题只有一个:让状态管理回归简单。
不需要学 Redux 的 action、reducer、middleware;不需要写 Context 的 Provider、useContext、useMemo;不需要操心性能优化、不需要在组件树里层层包裹。
创建一个 store,在需要的地方用,就这么简单。
参考资源
- Zustand 官方文档:github.com/pmndrs/zust...
- React Context 文档:react.dev/reference/r...