讲解
React 的状态管理选择多到出名,先建立分类坐标系再选型。服务端状态(接口数据:列表、详情)和客户端状态(UI 状态:主题、弹窗、表单)是两种东西——前者有缓存、过期、去重、竞态问题,交给 TanStack Query / SWR 这类数据请求库;后者才是传统「状态管理」的地盘。
客户端状态按复杂度阶梯选型。第一级:useState + 状态提升(父子/兄弟共享,够用就停)。第二级:useReducer + Context(跨层级、中等复杂,官方认可的零依赖方案)。第三级:专门库——Zustand(轻量、无 Provider、store 即 Hook,社区新宠)、Redux Toolkit(大型企业级、时间旅行调试、生态最全)、Jotai/Recoil(原子化,细粒度)。选型建议:中小项目 Zustand 或 useReducer+Context;老项目维护跟着现有栈走;需要严格审计追踪的大型团队协作才上 Redux。
Zustand 的用法简单到惊人:create 一个 store 就是定义一个 Hook——const useStore = create((set) => ({ count: 0, inc: () => set(s => ({ count: s.count + 1 })) })),组件里 const count = useStore(s => s.count)。选择器(selector)订阅让组件只在「自己关心的切片」变化时重渲染,没有 Provider 样板,没有 Context 的全员重渲染问题。
无论选什么,纪律通用:服务端状态别手动塞进全局 store(缓存失效会折磨你);全局 store 只放「真正全局」的状态,局部状态留在组件里;选型以团队熟悉度为先,状态库之间的差距远小于「用对/用错」之间的差距。
示例
手写一个迷你 Zustand:选择器订阅 + 精确通知(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 迷你 zustand:store = 状态 + 订阅;组件按选择器精确更新
function create(initializer) {
let state;
const listeners = new Set();
const set = (partial) => {
const next = typeof partial === 'function' ? partial(state) : partial;
state = { ...state, ...next };
listeners.forEach((fn) => fn(state)); // 通知所有订阅者
};
state = initializer(set);
return {
getState: () => state,
set,
subscribe: (fn) => {
listeners.add(fn);
return () => listeners.delete(fn);
},
};
}
// 组件模拟:只在选择器结果变化时「重渲染」
function useStore(store, selector) {
let selected = selector(store.getState());
let renders = 1;
store.subscribe((state) => {
const next = selector(state);
if (!Object.is(next, selected)) {
selected = next;
renders++;
}
});
return { get value() { return selected; }, renders: () => renders };
}
const store = create((set) => ({
count: 0,
theme: 'light',
inc: () => set((s) => ({ count: s.count + 1 })),
setTheme: (t) => set({ theme: t }),
}));
// 组件 A 只关心 count,组件 B 只关心 theme
const compA = useStore(store, (s) => s.count);
const compB = useStore(store, (s) => s.theme);
store.getState().inc();
store.getState().inc();
assert.strictEqual(compA.value, 2);
assert.strictEqual(compA.renders(), 3); // 初始 + 2 次更新
assert.strictEqual(compB.renders(), 1); // theme 没变,B 一次都没重渲染
store.getState().setTheme('dark');
assert.strictEqual(compB.value, 'dark');
assert.strictEqual(compB.renders(), 2);
assert.strictEqual(compA.renders(), 3); // A 不受 theme 影响
console.log('count 更新两次: A 渲染', compA.renders(), '次,B 渲染', compB.renders() - 1, '次(选择器精确订阅)');
console.log('最终状态:', JSON.stringify({ count: store.getState().count, theme: store.getState().theme }));
真实 Zustand 代码:
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
theme: 'light',
inc: () => set((s) => ({ count: s.count + 1 })),
toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
}));
function Counter() {
const count = useStore((s) => s.count); // 选择器订阅:只随 count 重渲染
const inc = useStore((s) => s.inc);
return <button onClick={inc}>{count}</button>;
}
function ThemeButton() {
const { theme, toggleTheme } = useStore((s) => ({ theme: s.theme, toggleTheme: s.toggleTheme }));
return <button onClick={toggleTheme}>当前 {theme}</button>;
}
常见坑
- 接口数据手动塞全局 store:缓存、过期、竞态全要自己写;服务端状态用 TanStack Query/SWR。
- useStore() 不加选择器拿整个 store:任何字段变化都重渲染,等于丢掉了精确订阅的意义。
- 选择器返回新建对象:
useStore(s => ({ a: s.a }))每次返回新引用导致无限重渲染;用单个字段选择器或 useShallow。 - 小项目硬上 Redux:三个组件共享用 Context 就够,工具复杂度要匹配问题复杂度。
- store 里放巨型嵌套对象:更新一层要展开五层;拍平结构(范式化)或按域拆多个 store。
小结
先分服务端/客户端状态;客户端状态按 useState → useReducer+Context → Zustand/Redux 阶梯升级;选择器精确订阅。下一章保障质量:组件测试。