状态管理选型决策
状态分类、四库核心对比、Context 边界、决策树与面试高频题
一、状态分类
不同类型的状态应使用不同的方案,不要用一个库管所有状态。
| 状态类型 | 特点 | 推荐方案 |
|---|---|---|
| 局部 UI 状态 | 只在单个组件内使用 | useState / useReducer |
| 跨组件共享 | 多组件需要读写同一状态 | Context / Zustand / Jotai |
| 服务端状态 | 来自 API,需要缓存和同步 | TanStack Query / SWR |
| URL 状态 | 需要序列化到 URL | Router searchParams |
| 表单状态 | 复杂验证和多步骤 | React Hook Form |
| 全局复杂状态 | 大型应用全局状态 | Zustand / Redux Toolkit |
二、四库核心对比
| 维度 | Zustand | Jotai | Redux Toolkit | Valtio |
|---|---|---|---|---|
| 心智模型 | 单 store(自上而下) | 原子化(自下而上) | 单 store + slice | Proxy 响应式 |
| Provider | ❌ 不需要 | ❌ 不需要 | ✅ 必须 | ❌ 不需要 |
| 更新方式 | set 函数 | setAtom | dispatch(action) | 直接修改对象 |
| 派生状态 | 手动 selector / 计算 | atom 原生支持 | createSelector | derive(第三方) |
| 异步 | 直接 async/await | 异步 atom + Suspense | createAsyncThunk | 直接 async/await |
| Immer | 中间件可选 | 不需要 | 内置 | 不需要 |
| DevTools | 中间件接入 | 插件 | 内置最强 | 插件 |
| 持久化 | persist 中间件 | atomWithStorage | 手动 / RTK Query | 手动 |
| 包体积 | ~1KB | ~2KB | ~11KB | ~3KB |
| TypeScript | 优秀 | 优秀 | 优秀 | 良好 |
| 学习曲线 | 极低 | 低 | 中等 | 极低 |
| 适合场景 | 中小型通用 | 细粒度状态 / 复杂派生 | 大型团队 / 严格规范 | Vue 背景 / 快速原型 |
三、同一需求的四种写法对比
计数器
jsx
// Zustand
const useStore = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}));
function Counter() {
const count = useStore((s) => s.count);
const increment = useStore((s) => s.increment);
return <button onClick={increment}>{count}</button>;
}
// Jotai
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
// Redux Toolkit
const slice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: { increment: (state) => { state.count++; } },
});
function Counter() {
const count = useSelector((s) => s.counter.count);
const dispatch = useDispatch();
return <button onClick={() => dispatch(slice.actions.increment())}>{count}</button>;
}
// Valtio
const state = proxy({ count: 0 });
function Counter() {
const snap = useSnapshot(state);
return <button onClick={() => { state.count++; }}>{snap.count}</button>;
}可以看到:
- Zustand:最接近 React 的
useState心智模型 - Jotai:代码最少,原子级定义
- Redux Toolkit:最规范,但步骤最多
- Valtio:最直觉,直接修改对象
四、Context 的正确使用边界
Context 不是"状态管理工具",而是"依赖注入机制"。
jsx
// ✅ Context 适合:低频变化、全局配置
const ThemeContext = createContext('light');
const I18nContext = createContext('zh-CN');
const AuthContext = createContext(null);
// ❌ Context 不适合:高频更新
// Context 值变化 → 所有消费者重渲染(无选择器)
const FormContext = createContext({ /* 频繁变化的表单数据 */ });
// 如果必须用 Context 传递状态,优化策略:
// 1. 拆分 Context(按更新频率分离)
// 2. useMemo 包裹 Provider value
// 3. 状态和 dispatch 分成两个 Context五、选型决策树
你需要什么?
│
├── 组件内部状态 → useState / useReducer
│
├── 少量跨组件共享 + 低频更新 → Context
│
├── API 数据(缓存、同步、后台刷新) → TanStack Query / SWR
│
├── 复杂表单 → React Hook Form
│
├── 客户端全局状态 →
│ │
│ ├── 想要最简单的 API → Zustand
│ │
│ ├── 有大量派生计算 / 原子化需求 → Jotai
│ │
│ ├── 大型团队 / 需要严格规范 / 强 DevTools → Redux Toolkit
│ │
│ ├── 喜欢可变风格 / Vue 背景 → Valtio
│ │
│ └── 不确定 → Zustand(最通用、最简单、社区最活跃)
│
└── 以上组合使用(常见)
例:Zustand(UI 状态)+ TanStack Query(API 数据)+ React Hook Form(表单)六、按项目规模推荐
小型项目(个人 / 小团队)
useState + Context + TanStack Query
或
Zustand + TanStack Query- 状态简单,不需要额外库
- TanStack Query 管理 API 数据
中型项目(5-15 人团队)
Zustand(客户端状态)+ TanStack Query(服务端状态)+ React Hook Form(表单)- Zustand 简洁够用,新人上手快
- 服务端状态和客户端状态分离
大型项目(15+ 人 / 微前端)
Redux Toolkit(全局状态)+ RTK Query 或 TanStack Query(API)+ React Hook Form- Redux 的严格规范保证代码一致性
- DevTools 在复杂调试中不可替代
- 微前端场景可通过 Redux 共享状态
面试高频题
Q: 为什么不推荐用 Context 做高频更新的状态管理?
Context 没有选择器机制,值变化时所有消费者都重渲染,无论是否用到了变化的部分。而 Zustand、Jotai 等库通过选择器或原子化实现细粒度订阅,只有用到的值变化时才重渲染。
Q: 服务端状态和客户端状态有什么区别?
- 服务端状态:来自 API,有缓存、过期、同步、竞态、乐观更新等需求。用 TanStack Query 等专用工具
- 客户端状态:只存在于前端,如 UI 状态、表单状态。用 useState / Zustand 等
把两者混在一个 store 里管理(如用 Redux 管 API 数据)是常见的反模式。
Q: Zustand、Jotai、Redux Toolkit、Valtio 怎么选?
| 需求 | 推荐 |
|---|---|
| 最简单、最快上手 | Zustand |
| 大量派生计算 | Jotai |
| 大型团队 + 强调规范 | Redux Toolkit |
| 可变风格 / Vue 背景 | Valtio |
| 不确定 | Zustand |
Q: 一个项目可以混用多个状态管理库吗?
可以且推荐。按场景选型:
useState/useReducer:局部状态- Zustand / Jotai:跨组件的客户端状态
- TanStack Query:服务端数据缓存
- React Hook Form:表单
- URL searchParams:URL 状态
它们各自管理不同类型的状态,不会冲突。