Zustand、Jotai 和 Redux Toolkit 应该怎样选择?
Zustand 用一个 hook 暴露整个 store,组件通过选择器订阅需要的部分,不需要 Provider;Jotai 把状态拆成原子,组件按原子订阅,派生原子自动追踪依赖;Redux Toolkit 要求通过 action 和纯 reducer 修改状态,约束最多,自带 DevTools、thunk 和开发期检查。服务端数据应交给查询库,三者只管客户端状态。
参考答案
简答
- Zustand:
create返回一个 hook,组件用选择器读取需要的字段,选择结果按Object.is比较决定是否重新渲染。不需要 Provider,store 上的getState、setState可以在 React 之外调用。 - Jotai:
atom只是定义,值保存在 store 中。派生原子的读函数里get会追踪依赖,依赖变化时自动重新计算。不使用 Provider 时所有组件共用默认 store。 - Redux Toolkit:状态只能通过 dispatch action 改变,reducer 是纯函数。
configureStore默认加入 thunk、连接 Redux DevTools,开发环境还会检查状态被直接修改和存入不可序列化的值。 - 选择看状态的形状和团队需要的约束:集中的业务状态、需要记录和回放操作时选 Redux Toolkit;状态零散、彼此有派生关系时 Jotai 更顺手;想要少量样板代码的集中 store 选 Zustand。
- 服务端数据由 TanStack Query 或 SWR 管理,参见《为什么要用 TanStack Query 或 SWR 管理服务端状态?》。
Zustand
JavaScript
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
const useCartStore = create((set) => ({
items: [],
coupon: null,
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
setCoupon: (coupon) => set({ coupon }), // 默认浅合并,其他字段保留
}));
// 只订阅 items 的长度,coupon 变化不会让它重新渲染
const count = useCartStore((state) => state.items.length);
// 选择器每次返回新对象时,要用 useShallow 按字段比较
const { items, addItem } = useCartStore(
useShallow((state) => ({ items: state.items, addItem: state.addItem })),
);
Zustand v5 用 Object.is 比较选择结果。选择器每次返回新对象而不包 useShallow,可能导致 "Maximum update depth exceeded"。也可以每个字段单独调用一次选择器。
Jotai
JavaScript
import { atom, useAtom, useAtomValue } from 'jotai';
const priceAtom = atom(100);
const countAtom = atom(2);
// 派生原子:priceAtom 或 countAtom 变化时自动重新计算
const totalAtom = atom((get) => get(priceAtom) * get(countAtom));
function Total() {
return useAtomValue(totalAtom);
}
原子要定义在模块顶层或用 useMemo 保持引用稳定,在渲染中直接创建会导致 useAtom 无限循环。需要让不同子树拥有各自的状态、注入初始值或通过重新挂载重置所有原子时,才需要 Provider。
Redux Toolkit
Redux Toolkit 的写法和 reducer 纯粹性见《Redux 的单向数据流为什么强调 reducer 的纯粹性?》。它的样板代码比前两者多,换来的是统一的修改入口:每次变化都有一个 action,可以在 DevTools 中逐条查看和回放,中间件可以统一处理日志、埋点和异步流程。
对比
| Zustand | Jotai | Redux Toolkit | |
|---|---|---|---|
| 状态组织 | 一个或几个集中 store | 分散的原子 | 一个全局 store,按 slice 划分 |
| 订阅粒度 | 选择器 | 原子 | 选择器(useSelector) |
| Provider | 不需要 | 可选 | 需要 <Provider store> |
| 修改方式 | 调用 set | 写原子 | dispatch action |
| 调试 | 可接 DevTools 中间件 | 有独立的调试工具 | 默认接入 DevTools |
常见追问
- SSR 时要注意什么?模块级的 Zustand store 和 Jotai 默认 store 会被同一个服务器进程里的所有请求共享,SSR 时要为每个请求创建独立的 store,或在 Jotai 中为每个请求使用独立的 Provider。
- 只用 React 自带的
useState和 Context 行不行?状态少、更新不频繁时可以。Context 值变化时所有使用它的组件都会重新渲染,状态多且更新频繁时才需要这些库的细粒度订阅。 - 三者能混用吗?可以,但同一份数据只应该有一个来源,否则需要在多处同步。