Zustand vs Jotai:原子化状态管理为什么在 React 生态中崛起

本文深入对比 Zustand 和 Jotai 的原子化状态管理思路,分析两者在外部 store 与原子依赖图上的核心差异,梳理各自适用场景、性能原理和常见误区,并给出 React 项目中状态管理选型的落地建议。

先重新认识状态管理:从 Context 到原子化

如果你在 2019 年前后开始写 React,大概率体验过 Context 带来的短暂惊喜。它让你不需要引入额外库就可以传递全局状态,但很快你就会发现,Context 的每一次变化都会让所有消费它的组件重新渲染。你开始做 memo、useMemo、拆分 Provider,依赖复杂的上下文分区来缓解性能压力。这类问题积累到一定程度,团队就会重新思考:状态管理工具到底在帮我们解决什么问题?

AI technology illustration

Zustand 和 Jotai 在这种背景下被广泛接受。它们常被归为“原子化状态管理”,核心思路都是让状态的最小粒度和组件订阅的粒度对齐。但两者的模型并不相同:Zustand 本质上是“可订阅的外部 store”,Jotai 则是“由原子组成的依赖图”。理解这个区别,你选型时会少很多纠结。

从 Redux 到原子化:发生了什么变化

Redux 的优秀之处在于强制统一数据流,所有修改都必须经过 action、reducer、dispatch。这让大型团队协作变得更可预测,但相应的,样板代码和概念负担也不小。很多团队发现,一个简单的弹窗状态也要写三个 action 文件,过度工程化逐渐成为问题。

Context API 出现后,一些中小型项目开始用 “useReducer + Context” 替代 Redux。然而 context 的订阅机制没有优化,只要 provider 的 value 改变,所有消费者都会重渲染,与性能优化天然冲突。于是,专门针对订阅粒度进行优化的库开始补位。

React 的渲染模型决定了组件必须通过某种机制感知状态变化。Context 的广播特性让任何值变化都会影响整个子树,而更细粒度的订阅需要状态库自行维护依赖。Zustand 和 Jotai 分别从“选择器”和“原子依赖”两个角度实现了这一点。前者像手动给每个组件画一条网线,后者像自动构建一张依赖网。这两条路线殊途同归,也给了开发者不同的心智模型。

Zustand:外部 store 加精确订阅

Zustand 的设计非常务实:把状态放在 React 之外的一个集中式 store 里,组件通过 hook 订阅自己需要的片段。底层依赖 React 18 的 useSyncExternalStore,确保在任何优先级渲染下都能拿到一致的状态,同时规避了 tearing 问题。

import { create } from 'zustand';
import { shallow } from 'zustand/shallow';

const useStore = create((set) => ({
  user: null,
  permissions: [],
  setUser: (user) => set({ user }),
  setPermissions: (permissions) => set({ permissions })
}));

function Header() {
  const user = useStore((s) => s.user);
  // 只订阅 user,permissions 变化不会触发这里
  return <div>{user?.name}</div>;
}

核心在于 selector 机制。每次渲染时,useStore 会取出返回值,并用默认的 Object.is 比较。如果返回一个新对象,比如直接返回整个 state,那效果就跟 useContext 一样,一改全重渲染。这时你需要传入 equalityFn,或者用 zustand 的 shallow 工具做浅比较。

const { user, permissions } = useStore(
  (s) => ({ user: s.user, permissions: s.permissions }),
  shallow
);

很多人说 Zustand 性能好,实际上它只是“按 selector 比较”,并不代表任意场景都优于其他库。如果 selector 写得不恰当,依然会有重复渲染。

Zustand 还有一个好特性:store 对象可以在任何地方使用,不一定要在组件内部。你可以用 createStore 创建原始 store,然后在 API 层、路由拦截器里直接调 getState 或 setState。这个能力让 Zustand 非常适合处理全局性、跨模块的状态。

Jotai:像做依赖图一样组织状态

Jotai 的模型来自 Recoil,但 API 更简洁。你定义 atom,它可以是基本状态的容器,也可以通过 get 函数组合出新的派生值。组件用 useAtom 消费,当某个 atom 变化时,React 只会重新渲染真正依赖它的组件。

import { atom, useAtom } from 'jotai';

const listAtom = atom([]);
const searchTextAtom = atom('');
const filteredListAtom = atom((get) =>
  get(listAtom).filter(item => item.includes(get(searchTextAtom)))
);

function List() {
  const [filteredList] = useAtom(filteredListAtom);
  // 仅当 filteredListAtom 的计算结果变化时才会重渲染
}

这个例子中,如果 listAtom 不变,只有 searchTextAtom 变化,filteredListAtom 会重新计算,进而触发依赖它的组件更新。依赖关系由运行时自动建立,不需要手动写订阅。

Jotai 还支持读写 atom,通过 read 和 write 定义更复杂的逻辑。同时官方还有 atomWithStorage、atomWithDefault、loadable 等工具,处理持久化和异步都非常方便。

import { atomWithStorage } from 'jotai/utils';

const themeAtom = atomWithStorage('theme', 'light');
// 这个 atom 会自动同步 localStorage

Jotai 的原子并不需要像 selector 那样担心返回值的稳定性。因为 atom 的依赖图记录的是“哪个原子被读取”,而不是“返回值是否相等”。只要依赖的原子实例不变,组件就不会无谓重渲染。但这不代表你可以放心地把一个大对象放在单个 atom 里,频繁替换大对象仍可能导致性能下降。

两者解决什么问题:两个真实场景

场景一:中后台系统。全局用户信息、站点权限、主题配置、接口缓存,这类状态跨页面共享,而且不一定只在组件内部被修改。拦截器要更新 token,日志模块要读取用户信息。Zustand 非常自然——在任意模块里调用 store 的 getState/setState 即可,不需要把它挂到 React 节点上。

场景二:复杂表单或可视化编辑器。字段之间有联动、校验、动态显隐,还有大量派生值。如果用集中式 store,每个联动关系都要手动写订阅和更新,selector 越堆越多。Jotai 可以把每个字段当作 atom,把计算结果作为派生 atom,数据流自动建立,局部性极好。很多编辑器类应用的作者会选择 Jotai,正因为这种模型符合“局部状态 + 派生值密集”的形态。

方案对比:一张表说明差异

对比项 Zustand Jotai
模型核心 外部 store + 手动 selector atom 原子 + 自动依赖追踪
渲染优化 依赖 selector 返回值比较 依赖原子依赖图自动触发
组件外访问 直接 store.getState / setState 需要 store API 或跨模块同步
异步处理 异步 action 轻量直接 async atom + loadable 等工具
学习曲线 平坦,接近 useState + useReducer 需要适应原子派生思维
最佳场景 全局共享、跨模块、权限会话 组件内复杂局部状态、派生密集

这张表不是定论,但能帮你快速排除不适合的方向。

常见误区:原子化不是性能护身符

  • 误区一:Zustand 一定比 Context 快。Context 慢是因为所有消费者重渲染,而 Zustand 靠 selector 避免这一点。但如果你每次返回整个 store,它和 Context 没有本质区别。
  • 误区二:Jotai 可以完全替代 Redux。Redux 的 devtools、中间件生态、中心化调试能力依然强。Jotai 更适合模块级状态,团队需要严格可预测性时,Redux 仍然有优势。
  • 误区三:原子粒度越小越好。如果每个字符串都单独建 atom,代码会被拆成碎片。一个对象作为 atom 完全合理,只要更新时保证替换整个对象,依赖图依然精确。

如何选择:我的工程建议

先看状态之间的关系。如果是“共享被访问”为主,比如多个页面读同一份权限数据,选 Zustand;如果是“组合派生”为主,比如编辑器里多字段联动,选 Jotai。

如果是大型项目,两个库可以共存。我的习惯是:用 Zustand 管理应用级数据——登录用户、权限点、国际化、主题;用 Jotai 管理页面内复杂交互——编辑器选区、表单正在编辑的字段、筛选器组合。两者边界清楚,互不干扰。

很多团队还会引入一条判断清单,更直观:

  • 需要管理全局且跨模块的状态吗?——优先 Zustand
  • 页面内部有大量派生状态和联动吗?——优先 Jotai
  • 团队熟悉 Redux 的集中式模型吗?——Zustand 迁移成本更低
  • 想要最少的样板代码吗?——两者都很轻,但 Jotai 需要先理解 atom

另外,不要把状态管理库当作仓库,什么临时数据都往里面塞。URL 参数、浏览器存储、服务端缓存都有各自的合适位置。塞得越多,状态树越复杂,最后又要回到 Redux 那套严谨约束来维护。

最后说点心里话

Zustand 和 Jotai 的崛起,本质上是开发者在 Redux 的重量和 Context 的无能之间找到的折中。它们没有发明革命性概念,而是把响应式思想用 React 的方式实现得更干净。理解它们的关键不在背 API,而在识别自己的状态依赖形态。状态管理没有银弹,但当你看到错误模型带来的重复渲染和代码粘稠时,自然就明白该换工具了。

原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/710/

(0)
上一篇 1小时前
下一篇 1小时前

相关推荐