Jotai — 原子化 React 状态管理
是什么
Jotai 是 React 的状态管理库,把全局状态拆成无数小”原子”(atom),组件只订阅它真正用到的原子。
日常类比:zustand 是一个大账本(store),所有人翻同一本,找自己要的那一行;Jotai 是每个数据一张小卡片,谁需要哪张拿哪张,互不打扰。
你写:
const countAtom = atom(0)就声明了一个最小的状态单元。任何组件可以:
const [count, setCount] = useAtom(countAtom)去读、去改这颗原子。组件只订阅这一颗——别的 atom 改了不会触发重渲染。
为什么重要
不理解 atomic 状态管理,下面这些事都说不清:
- Recoil 的精神继承:Recoil 是 Facebook 2020 推出的 atom 状态库,但已停更。Jotai 接住了 atomic 思想,做得更小、更专注、更稳定。
- 比 zustand 更细粒度:zustand 要写 selector 才能避免无关重渲染;Jotai 自带 atom 级订阅,派生 + 订阅都自动。
- 与 React 18 Suspense 原生兼容:atom 可以是异步的(返回 Promise),Suspense 会自动接住 loading 状态。
- TypeScript-first:atom 类型自动推,写
atom(0)就推出PrimitiveAtom<number>,不用手动标注。
核心要点
Jotai 只有 三个核心概念:
-
atom:最小状态单元。可以是一个原子值(
atom(0)),也可以是一个派生公式(atom((get) => ...))。每个 atom 是一张独立的卡片。 -
useAtom:组件订阅 + 修改 atom 的钩子。返回
[value, setter],签名和useState几乎一样。 -
Atom 派生:一个 atom 可以基于其他 atoms 计算出来——你写公式,Jotai 自动追踪依赖。某个被依赖的 atom 改了,派生 atom 自动重算。
三件事拼起来就是 Jotai 的全部公开 API。其他 utils(atomFamily、atomWithStorage 等)都是这三件事的组合。
实践案例
案例 1:基础 atom
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
function Counter() { const [count, setCount] = useAtom(countAtom) return <button onClick={() => setCount(count + 1)}>{count}</button>}逐部分解释:
atom(0)在组件外创建一个原子,初始值 0useAtom(countAtom)订阅这颗原子,写法和 useState 一样- 任何调用
setCount的组件都会通知所有订阅countAtom的组件
案例 2:派生 atom(自动追踪依赖)
const countAtom = atom(0)const doubledAtom = atom((get) => get(countAtom) * 2)
function Display() { const [doubled] = useAtom(doubledAtom) return <div>{doubled}</div>}只要 countAtom 变化,doubledAtom 自动重算——你不需要写 selector、不需要写 useMemo、不需要在依赖数组里列。
get 函数在 read 回调里调用,Jotai 在运行时记录”我读了哪些 atom”,构成依赖图。
案例 3:异步 atom(Suspense 自动处理 loading)
const idAtom = atom(1)const userAtom = atom(async (get) => { const id = get(idAtom) const res = await fetch(`/api/users/${id}`) return res.json()})
function User() { const [user] = useAtom(userAtom) return <div>{user.name}</div>}
function App() { return ( <Suspense fallback={<Loading />}> <User /> </Suspense> )}userAtom 返回 Promise,Jotai 把它包装成 Suspense 兼容的资源——loading 期间外层 <Suspense> 自动显示 fallback,无需手写 if (loading) 分支。
踩过的坑
-
atom 必须在组件外定义:写在组件内部每次渲染都新建 atom,订阅链路全部丢失,状态会”莫名重置”。要动态创建用
atomFamily。 -
大量 atom 时调试难:没有 Redux DevTools 那种全局状态树视图。要装 jotai-devtools 独立包,并给 atom 加
debugLabel,才能在面板里识别每颗原子。 -
atom-in-atom 模式容易内存泄漏:
atomFamily默认无限缓存,参数空间大(比如按 userId 创建)时 Map 会一直涨。要手动setShouldRemove配 GC 策略。 -
Provider scope 误用:默认无 Provider 时全局共享一个 default store;要做”每个子树独立状态”(比如多个隔离的弹窗)必须用
<Provider>包裹。一旦混用,状态会串店。
适用 vs 不适用场景
适用:
- 中小到中型 React 应用,状态拆成多个独立单元
- 需要异步状态 + Suspense 集成的场景
- 想避开 Redux / Redux Toolkit 重型样板代码
- TypeScript 项目,希望少写类型标注
不适用:
- 大型企业应用且团队习惯 Redux 中间件 / dispatch action 流程 → 还是 Redux Toolkit 好
- 需要丰富的开发者工具、time travel 调试、action 日志 → Redux DevTools 还是更强
- 状态都是少数几个全局 slice,不需要 atom 级粒度 → zustand 更轻
学到什么
- 状态可以”原子化”——不是所有数据都该塞进同一个 store;按业务把状态切碎,订阅粒度自然到位
- 派生状态不必手写 selector——
atom((get) => ...)让运行时帮你追踪依赖,比useMemo+ 手写依赖数组省心 - atom 是”引用 identity”——这是底层 Map 的 key,所以 atom 必须 stable(在组件外定义、或用 atomFamily 缓存)
- 三套同作者状态库的对照:zustand / valtio / Jotai 出自同一个人(Daishi Kato),分别选了”集中 store + selector”、“Proxy mutate”、“atom 分散”三条路——理解 Jotai 反过来加深对前两套的理解
延伸阅读
- 官方文档:jotai.org(核心 API 一页讲完,文档非常薄)
- 入门视频:YouTube 搜 “Jotai vs Zustand vs Redux”(社区横评,快速建立心智模型)
- 源码精读:
src/vanilla/atom.ts+src/vanilla/store.ts(vanilla 部分核心约 500 行,dependency tracking 在readAtomState) - zustand —— 同作者的”集中 store + selector”路线
- react-19 —— Suspense + atom 的协作前提
关联
- zustand —— 同作者第一套,集中 store + selector,对照组
- react-19 —— atom 异步 + Suspense 自动 loading 依赖 React 18+ 的能力
- typescript —— atom 类型自动推,是 TS-first 设计的范例
反向链接
- immer —— Immer — 用 Proxy 让你写”看起来可改”的代码却产出不可变状态
- nanostores —— nanostores — 不到 1 KB 的”框架无关”状态库
- react-hook-form —— react-hook-form — input 不进 React state 也能写表单
- valtio —— valtio — 让 state.x++ 直接驱动 React 重渲染的 Proxy 状态库
- xstate —— XState — 把状态画成图,让矛盾写不出来
- zustand —— Zustand — 极简 React 状态管理