前端知库

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 中逐条查看和回放,中间件可以统一处理日志、埋点和异步流程。

对比

ZustandJotaiRedux 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 值变化时所有使用它的组件都会重新渲染,状态多且更新频繁时才需要这些库的细粒度订阅。
  • 三者能混用吗?可以,但同一份数据只应该有一个来源,否则需要在多处同步。