React 状态管理选型:Zustand、Redux Toolkit 与 Jotai
学完这篇你能拿到一套可直接照抄的选型方法:先看清三个库的状态模型差异,再各写一段最小可运行代码,最后按项目规模和数据形态做决定,而不是跟着热度走。
第一步:先用两个问题缩小范围
不要一上来就比 API。先问自己两件事:
- 这个状态是服务端数据还是客户端数据? 接口返回的列表、详情一律交给 TanStack Query 或 RTK Query,三个库都不该管。
- 状态之间的派生关系复杂吗? 如果只是「一个数组 + 几个操作函数」,Zustand 够用;如果是「A 变 B 变 C,C 还要组合 D」,Jotai 的原子模型更省心;如果要多人协作、要严格规范和时间旅行调试,Redux Toolkit 更稳。
第二步:Zustand 最小示例
import { create } from 'zustand'
export const useCartStore = create((set, get) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
total: () => get().items.reduce((n, i) => n + i.price, 0),
}))
组件里按需订阅:
const items = useCartStore((s) => s.items)
const add = useCartStore((s) => s.add)
没有 Provider,没有 action type,没有 reducer 样板。需要持久化加 `persist` 中间件,需要 Redux DevTools 加 `devtools` 中间件,都是一行。
注意:选择器如果返回新对象或新数组(比如 `useCartStore((s) => s.items.filter(...))`),每次渲染都会判定为「变了」,Zustand 会抛无限循环警告。解决办法是 `import { useShallow } from 'zustand/react/shallow'` 包一层,或者让选择器只返回原始值。
第三步:Redux Toolkit 最小示例
import { configureStore, createSlice } from '@reduxjs/toolkit'
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
add(state, action) { state.items.push(action.payload) }, // Immer 允许这么写
},
})
export const { add } = cartSlice.actions
export const store = configureStore({ reducer: { cart: cartSlice.reducer } })
入口文件挂 Provider:
import { Provider } from 'react-redux'
<Provider store={store}><App /></Provider>
组件里取数和派发:
const items = useSelector((s) => s.cart.items)
const dispatch = useDispatch()
dispatch(add({ id: 1, price: 20 }))
RTK 的价值不在「少写代码」,而在统一约定:状态怎么切、异步怎么走(`createAsyncThunk` 或 RTK Query)、谁改了数据(DevTools 时间旅行 + action 日志)。团队超过三五个人、状态跨十几个页面时,这套约束能省下大量争论。
注意:Immer 只在 `createSlice` 的 reducer 内部生效。在 reducer 外面拿到 state 后直接 `state.items.push(...)` 会报错,必须先 `JSON.parse(JSON.stringify())` 或 `structuredClone` 复制一份。
第四步:Jotai 最小示例
import { atom, useAtomValue } from 'jotai'
export const itemsAtom = atom([])
export const totalAtom = atom((get) =>
get(itemsAtom).reduce((n, i) => n + i.price, 0)
)
function Total() {
const total = useAtomValue(totalAtom)
return <span>{total}</span>
}
`totalAtom` 是派生原子,`itemsAtom` 一变它自动重算,且只有用到 `totalAtom` 的组件重渲染。这是 Jotai 相比前两者的核心差异——更新粒度天然细化,不需要手写选择器和 `useShallow`。
注意:`atom()` 必须写在模块顶层。写在组件函数里面,每次渲染都会创建一个全新原子,状态永远对不上,而且很难排查。异步原子配 `<Suspense>` 使用时记得给 fallback。
第五步:按场景拍板
| 场景 | 选择 |
|---|---|
| 个人项目、后台小工具、只想要一个全局 store | Zustand |
| 中大型团队、状态多、要调试审计、已用 Redux 生态 | Redux Toolkit |
| 派生关系复杂、高频更新、编辑器/画布/表单联动 | Jotai |
三条补充建议:
- 不要三个一起上。真要混,也最多是 RTK 管全局业务数据 + Jotai 管某个高频交互区域,并且中间用一层适配函数隔开。
- 别把接口数据塞进这些 store。缓存、重试、失效交给 TanStack Query / RTK Query,否则你会手写一遍它们的活。
- 迁移成本远低于预期:Zustand 和 Jotai 都是几百字节级的库,一个页面里可以先局部试用,跑通再扩大,不需要全量重写。
小结
- 先用「服务端数据还是客户端数据」「派生关系复杂不复杂」两个问题缩小范围。
- Zustand:无 Provider、样板最少,注意选择器返回新引用时用 `useShallow`。
- Redux Toolkit:约束强、工具链全,注意 Immer 只在 reducer 内生效。
- Jotai:原子 + 派生原子,重渲染粒度最细,注意 atom 定义在模块顶层。
- 服务端数据一律交给 TanStack Query / RTK Query,不要塞进这三个库。
- 拿不准就先用 Zustand,真的撞到墙再换,成本可控。
转载请注明出处,版权归原作者所有。
见习用户






