学完这篇你能拿到一张明确的选型决策表:知道 Redux、Zustand、Jotai、Recoil 四者到底差在哪、各自适合什么项目,以及新项目该用哪个、老项目怎么迁。
第一步:先分清四种「状态模型」
选型踩坑,八成是因为把它们当成「四个同类库」来比,其实底层模型完全不同:
- Redux(现在指 Redux Toolkit):单一 store + action + reducer 的严格单向数据流。状态只能通过 dispatch 变更,中间件、DevTools 时间旅行、可序列化约束都是强制的。约束多,换来的是团队协作时「谁都能看懂」。
- Zustand:也是单一 store,但就是一个普通对象 + 订阅,
create 一个 hook 就完事,不需要 Provider、不需要 action type、不需要 reducer。状态更新直接调用 store 里的函数。
- Jotai:自下而上的原子化(atom)。没有全局 store 这个概念,状态被切成一个个 atom,组件用到哪个订阅哪个,派生状态用
atom((get) => ...) 组合。
- Recoil:同样是原子化模型,atom + selector,思路和 Jotai 接近。
关键区别是:Redux / Zustand 是「自上而下的容器」,Jotai / Recoil 是「自下而上的拼装」。容器模型适合业务边界清晰的中后台,原子模型适合状态零碎、派生逻辑多的交互型页面。
注意:Recoil 从 2023 年起基本停止维护,React 19 下已有兼容性报告。新项目不要选它;老项目建议按下面的迁移一节处理。
第二步:同一功能写一遍,感受 API 差异
以计数器为例,四种写法分别是:
Redux Toolkit
// store.js
import { configureStore, createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: { inc: (state) => { state.value += 1 } },
})
export const { inc } = counterSlice.actions
export const store = configureStore({ reducer: { counter: counterSlice.reducer } })
// 组件里
const value = useSelector((s) => s.counter.value)
const dispatch = useDispatch()
Zustand
import { create } from 'zustand'
export const useCounter = create((set) => ({
value: 0,
inc: () => set((s) => ({ value: s.value + 1 })),
}))
// 组件里
const value = useCounter((s) => s.value)
const inc = useCounter((s) => s.inc)
Jotai
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const doubleAtom = atom((get) => get(countAtom) * 2)
// 组件里
const [count, setCount] = useAtom(countAtom)
Recoil
import { atom, useRecoilState } from 'recoil'
const countState = atom({ key: 'count', default: 0 })
// 组件里(还需在根节点包 RecoilRoot)
const [count, setCount] = useRecoilState(countState)
同样一个计数器:Redux 要 4 个概念(slice / action / reducer / dispatch),Zustand 只要 1 个 create,Jotai 只要 1 个 atom。项目越大,这个比例会反过来——Redux 的概念成本换来的是「任何一个人进来都能定位状态改动」。
第三步:按项目实际情况对号入座
| 场景 | 推荐 |
|---|
| 中大型团队、多人协作、状态复杂、需要严格可追溯 | Redux Toolkit |
| 中小项目、想快速上手、需要在组件外读写状态 | Zustand |
| 状态碎片化、派生计算多、想极致控制重渲染 | Jotai |
| 存量 Recoil 项目 | 继续用,同时规划迁移 |
补充几个容易忽略的判断点:
- 需要在组件外访问状态(axios 拦截器里读 token、路由守卫里判断登录):Zustand 直接
useStore.getState() 最舒服;Redux 要 store.getState(),也行但啰嗦;Jotai 需要 store 实例,稍绕。
- 服务端状态:四个库都该用来放「客户端状态」。列表数据、详情数据交给 TanStack Query / SWR,别塞进全局 store 手动管理 loading 和缓存。
- 表单状态:优先 react-hook-form 这类局部方案,不要为了表单再引入一个全局状态库。
注意:不要用「代码行数少」当唯一理由。Zustand 一个文件写 20 个 action 后,没有 action type 约束,改名和排查会变难——团队超过 3 人、模块超过 10 个时,这层约束是要还的。
第四步:重渲染的三个坑
- Zustand 选对象要浅比较。
useStore((s) => ({ a: s.a, b: s.b })) 每次返回新对象,必重渲染。用 useShallow:
import { useShallow } from 'zustand/react/shallow'
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })))
- Redux 的
useSelector 默认用 === 比较,返回新数组/新对象同样会重渲染,改用 shallowEqual 或 createSelector 记忆化。
- Jotai 的粒度优势要真的用起来。把一个大 atom 拆成多个小块,否则等于回到全局 store 的思路,还多背了一套概念。
第五步:迁移路径
- Redux → Zustand:不用一次性重写。新模块直接用 Zustand,老 slice 保留;在组件里逐步把
useSelector 换成 Zustand hook,两者可以共存。等 Redux 只剩空壳再删依赖。
- Recoil → Jotai:API 形态接近,
atom({key, default}) 换成 atom(default),selector 换成派生 atom,useRecoilState 换成 useAtom,去掉 RecoilRoot。逐个模块迁移即可。
- 任意 → Redux Toolkit:只在「状态需要被记录、回放、跨团队审查」时才值得反向迁。
注意:迁移时先把项目里的「服务端数据」抽出去给 TanStack Query,再迁客户端状态。否则你会把一堆缓存逻辑原样搬进新库,迁了个寂寞。
小结
- 四者的根本差别在模型:Redux / Zustand 是自上而下的容器,Jotai / Recoil 是自下而上的原子。
- 团队大、状态复杂、要可追溯 → Redux Toolkit;中小项目、要轻快、要组件外访问 → Zustand;状态零碎、派生多 → Jotai;Recoil 新项目别选。
- 服务端数据交给 TanStack Query,别塞进全局 store。
- 性能三个必查点:Zustand 用
useShallow、Redux 用浅比较或记忆化、Jotai 真的拆细 atom。
- 迁移可以共存渐进,先抽服务端数据,再换客户端状态。