在 React 应用里,状态管理几乎是所有中大型项目的第一道分水岭。组件树一旦变深,props 层层透传就会让人崩溃;而随便选一个全局状态库,又可能在半年后陷入模板代码与性能陷阱。本文聚焦当前最主流的三款 React 状态方案——Zustand、Jotai 与 Redux Toolkit,从设计哲学、代码写法和选型决策三个维度拆解,帮你结合项目体量做出清醒选择。
三款库的设计哲学
Zustand:极简的 hooks 风格
Zustand 的核心思想是把”store”做成一个返回状态的 hook。它没有 Provider 嵌套,也没有 action 类型常量,你直接在一个函数里定义状态和修改函数,组件用 hook 订阅自己关心的字段即可。这种”少即是多”的取向,让它成为从中小项目到大型应用都能平滑使用的默认选择。
Jotai:自底向上的原子模型
Jotai 受 Recoil 启发,提出 atom(原子)概念:每个状态都是一个最小单元,组件只订阅自己用到的 atom。当某个 atom 变化,只有依赖它的组件重渲染,天然避免不必要的刷新。它特别适合状态高度分散、彼此耦合少的场景。
Redux Toolkit:可预测的状态容器
Redux Toolkit(RTK)是 Redux 官方推荐的现代写法,用 createSlice 把 action、reducer、初始状态收敛到一个函数里,并用 Immer 让你”直接改”状态。它的强项在于可预测性、时间旅行调试,以及 RTK Query 对服务端数据的统一管理——大型团队协作时价值明显。
同一个计数器,三种写法
用同一个需求对比代码最直观:维护一个计数器和它的自增方法。
// Zustand:一个 create 搞定状态与修改函数
import { create } from 'zustand'
const useCounter = create((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}))
function Counter() {
const count = useCounter((s) => s.count)
const inc = useCounter((s) => s.inc)
return <button onClick={inc}>{count}</button>
}
// Jotai:状态拆成最小 atom,组件只订阅用到的
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const incAtom = atom(null, (get, set) => set(countAtom, get(countAtom) + 1))
function Counter() {
const [count, ] = useAtom(countAtom)
const [, inc] = useAtom(incAtom)
return <button onClick={inc}>{count}</button>
}
// Redux Toolkit:slice 收敛 action/reducer,Immer 直接改
import { createSlice, configureStore } from '@reduxjs/toolkit'
import { useSelector, useDispatch } from 'react-redux'
const slice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
inc: (state) => { state.count += 1 },
},
})
const store = configureStore({ reducer: { counter: slice.reducer } })
const { inc } = slice.actions
function Counter() {
const count = useSelector((s) => s.counter.count)
const dispatch = useDispatch()
return <button onClick={() => dispatch(inc())}>{count}</button>
}
异步数据怎么管
真实项目里状态多半来自接口,三款库的处理方式各异:
- Zustand 直接在 store 里写 async 函数,配合 set 更新即可,轻量直观。
- Jotai 用 atom 的异步取值或 jotai/query 扩展做数据请求。
- Redux Toolkit 提供 RTK Query,声明式地描述接口,自动处理缓存、轮询、失效,适合接口多的中后台。
// RTK Query:声明接口,自动管缓存与失效
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (b) => ({
getUser: b.query((id) => ({ url: `/users/${id}` })),
}),
})
// 组件内:const { data, isLoading } = useGetUserQuery(1)
重渲染与选择器:性能关键点
全局状态最容易被忽视的成本是”不必要的重渲染”。三款库都依赖选择器(selector)来精确订阅:Zustand 靠传给 hook 的 selector 函数,Jotai 天然按 atom 粒度,RTK 则用 useSelector 配合比较函数。关于 React 重渲染与 Hooks 优化的更多细节,可参考React 性能优化实战。
// Zustand:只订阅 count,其他字段变化不会触发本组件刷新
const count = useCounter((s) => s.count)
// 用 shallow 比较对象/数组,避免引用变化导致误刷新
import { shallow } from 'zustand/shallow'
const { a, b } = useCounter((s) => ({ a: s.a, b: s.b }), shallow)
选型决策表
| 维度 | Zustand | Jotai | Redux Toolkit |
|---|---|---|---|
| 上手成本 | 极低,几分钟可用 | 低,需理解 atom 思维 | 中等,概念较多 |
| 包体积(压缩) | 约 1KB | 约 5KB | 约 10KB,RTK Query 更大 |
| 重渲染控制 | 选择器细粒度 | 原子级自动 | useSelector + shallowEqual |
| 服务端数据 | 自行封装 async | jotai/query 扩展 | RTK Query 开箱即用 |
| 调试与生态 | 中等 | 中等 | DevTools 时间旅行、生态最全 |
| 适合规模 | 小到大型皆可 | 中大型、状态分散 | 中大型团队、接口多 |
常见坑与避坑清单
| 坑 | 现象 | 解法 |
|---|---|---|
| Zustand 全量订阅 | 顶层 useStore() 不传 selector,任意字段变都重渲染 | 用选择器只取所需字段 |
| Jotai atom 放组件内 | 每次渲染新建 atom,状态被重置 | 把 atom 提到模块作用域 |
| RTK 忘记注册 reducer | 组件 dispatch 无反应 | 单一 store + combineReducers 注册 |
| 服务端数据塞 store 不失效 | 缓存陈旧、重复请求 | 用 RTK Query / React Query / SWR |
什么时候其实不需要它们
如果状态只活在一个组件内,用 useState / useReducer 就够了;如果只是为了读取接口数据,React Query / SWR 比把数据塞进全局 store 更专业。引入全局状态库的前提是:多个不相邻的组件需要共享同一份状态。包体积上,前端构建工具演进里也提到按需引入对体积的影响;而把状态持久化到 localStorage 以跨刷新保留时,可与前端缓存策略配合落地。
小结
没有”最好的”状态库,只有”最合适的”。原型和小工具直接用 Zustand;状态高度分散、追求极致细粒度更新选 Jotai;中大型团队协作、接口多且要强约束,Redux Toolkit + RTK Query 更稳。选型时先问自己三个问题:团队规模、状态耦合度、是否大量服务端数据——答案往往就清晰了。




