目录
- 前言
- 一、Jotai 概览
- 1、Jotai 的定义
- 2、核心概念与最小示例
- 二、Jotai 的 API 解读
- 1、核心 API(Jotai 的最基础能力)
- (1)、atom() —— 创建一个最小可独立更新的状态单元
- (2)、useAtom() —— 读写 atom 的主要方式
- (3)、useAtomValue()、useSetAtom()(解耦读写)
- ①、useAtomValue(atom)
- ②、useSetAtom(atom)
- (4)、Provider —— 提供一个独立的 Atom Store
- 2、原子增强 API(给 atom 加能力)
- (1)、atomWithDefault(initializer)
- (2)、atomWithReducer(atom, reducer)
- (3)、atomWithStorage(key, initialValue)
- (4)、atomWithReset(initialValue)
- 3、派生(computed)与异步 API
- (1)、派生 atom(读函数)
- (2)、可写派生 atom(读函数 + 写函数)
- (3)、异步 atom(async)
- (4)、异步可写原子(异步 action)
- 4、Store 级 API(高级)
- (1)、createStore() —— 手动创建独立 store
- (2)、useStore()
- 5、开发者工具 API
- (1)、atom.onMount(set => ...) —— 原子的生命周期
- (2)、useAtomCallback()(在回调中读取/写入 atom)
- 三、Jotai 的思维模式总结(重要 ⭐️)
- 四、性能细节与最佳实践
- 五、TypeScript 使用技巧(类型安全)
- 六、扩展中间件与生态(常见插件)
- 七、SSR(服务端渲染)与 hydration
- 八、Persist(本地持久化)
- 九、并发、Suspense 与错误边界(React 18/19)
- 十、测试策略
- 十一、高级技巧与模式
- (1)、表单模式(local draft + submit)
- (2)、Optimistic update(乐观更新)
- (3)、Derived cache(缓存派生)
- (4)、atom factories + dependency injection
- 十二、与其它状态库的对比(选择建议)
- 十三、常见问题
- 十四、16. 进阶:atomic design 与模块化架构示例(企业级)
前言
Jotai 官方文档
Jotai 的核心设计深度绑定 React 的 Hooks 机制和其调度器(Scheduler)。它的“原子”概念、读写操作以及衍生状态的计算,都是围绕 React 的渲染生命周期和 Hooks 规则构建的。将其移植到 Vue,几乎相当于重写整个库,而不是简单地做一个适配层。因此,目前没有成熟、官方的 Vue 版本。
| 特性 | Zustand | Jotai |
|---|---|---|
| 设计哲学 | 集中式、自上而下。基于单一、集中的Store组织状态和逻辑。 | 原子化、自下而上。将状态分解为最小独立单元(Atom),可自由组合。 |
| 心智模型 | 类似“全局可观测的对象”,适合有组织、模块化的应用状态。 | 类似“React自己的useState,但可全局共享”,更贴近组件化思维。 |
| 核心优势 | 极简API、开箱即用的状态切片与优化、易于在React外部使用。 | 极致的细粒度响应性、自动计算派生状态。 |
| 性能特点 | 通过选择器实现精准订阅,避免不必要的重渲染。 | 原子级的依赖追踪,确保只重渲染真正相关的组件。 |
| 最佳适用场景 | 大多数具有明确模块边界的中大型应用(如后台管理、电商平台)、需要从Redux迁移的场景。 | 状态高度分散、依赖关系复杂(如复杂表单、富文本编辑器)、UI变化频繁的应用。 |
| 不适合的场景 | 状态极度分散、快速变化的UI场景。 | 需要高度中心化、强约束状态流转模式的应用。 |
在未来的 React 生态里,Zustand 和 Jotai 很可能会长期共存,甚至在同一项目中互补使用。
例如:
- 用 Zustand 管理核心业务模块的共享状态
- 用 Jotai 处理 UI 交互、表单等局部复杂状态
一、Jotai 概览
1、Jotai 的定义
Jotai(日文“原子”的意思)是一个基于原子(atom)模型的 React 状态管理库,主张最小 API、按需订阅、可组合的原子与派生(derived)状态。
核心卖点:
- 极简 API:atom、useAtom、Provider 就能做大多数事。
- 按需订阅:组件只会因为它依赖的 atom 变更而渲染。
- 可组合性:派生 atom 很容易组合、复用。
- 与 React Hooks 自然契合:原生 Hook 使用体验。
- 生态:有 jotai/utils、jotai/immer、jotai/devtools、jotai/query 等扩展。
适合场景:中大型应用的局部/全局状态管理、微状态组合、对细粒度重渲染有高要求的 UI 场景、逐步从 Context/Redux 迁移的项目。
2、核心概念与最小示例
import{atom,useAtom}from'jotai';// 定义原子(最小状态单元)constcountAtom=atom(0);// 在组件中读取与写入functionCounter(){const[count,setCount]=useAtom(countAtom);return<button onClick={()=>setCount(c=>c+1)}>{count}</button>;}关键点:
- atom(initial):创建可读写 atom(也可以创建只读 atom)。
- useAtom(atom):返回 [value, setValue]。组件只订阅它使用的 atom。
- atom 是可组合、可传入参数(通过工厂模式或 atomFamily 实现)的。
二、Jotai 的 API 解读
1、核心 API(Jotai 的最基础能力)
(1)、atom() —— 创建一个最小可独立更新的状态单元
constcountAtom=atom(0)原子是什么?
- Jotai 的最核心抽象:可订阅、可更新的状态。
- 类似 Redux 的 store 片段,但 天然支持细粒度渲染。
- 每个 Atom 自动支持订阅,不需要 selector。
| 形式 | 说明 |
|---|---|
atom(value) | 原子存普通值 |
atom(get => ..., (get, set, update) => ...) | 派生/可写原子 |
(2)、useAtom() —— 读写 atom 的主要方式
const[count,setCount]=useAtom(countAtom)- 自动订阅 atom
- Atom 更新后只使使用它的组件重渲染(细粒度)
关键:为什么 Jotai 更新只重渲染使用 atom 的组件?
因为原子本身是订阅对象,组件只订阅它依赖的 atom,不会像 Redux Provider 那样全局 root 订阅。
(3)、useAtomValue()、useSetAtom()(解耦读写)
①、useAtomValue(atom)
只读:
constcount=useAtomValue(countAtom)②、useSetAtom(atom)
只写:
constsetCount=useSetAtom(countAtom)解耦后可以避免多余渲染、减少 props 传递。
(4)、Provider —— 提供一个独立的 Atom Store
<Provider><App/></Provider>在 Jotai 中,Provider 就是一个“状态容器”。
特点:
- 你可以创建多个 Provider(与 Redux 必须单例不同)
- 每个 Provider 内的 atom 状态互不影响
- 支持多窗口、嵌套组件、可重置的状态域
2、原子增强 API(给 atom 加能力)
(1)、atomWithDefault(initializer)
动态默认值(初次 get 才计算)
constcountAtom=atomWithDefault((get)=>get(baseAtom)*2)适用:
- 惰性计算
- 依赖其他 atom 的默认值
- 延迟初始化(如 localStorage)
(2)、atomWithReducer(atom, reducer)
Redux-style reducer 写法:
constcountAtom=atom(0)constreducerAtom=atomWithReducer(countAtom,(state,action)=>{if(action==='inc')returnstate+1returnstate})你甚至可以把 Redux reducer 拿来直接复用。
(3)、atomWithStorage(key, initialValue)
自动同步 localStorage / sessionStorage
constthemeAtom=atomWithStorage('theme','light')Jotai 对持久化做得非常优雅:
- 自动同步多 tab
- 支持自定义 storage adapter
(4)、atomWithReset(initialValue)
带 reset 功能的 atom:
constcountAtom=atomWithReset(0)constsetCount=useSetAtom(countAtom)setCount(RESET)// 重置为 0与业务里的“一键清空表单”“退出登录”特别适配。
3、派生(computed)与异步 API
(1)、派生 atom(读函数)
constdoubleCountAtom=atom((get)=>get(countAtom)*2)派生 atom 的特点:
- 任何 atom 改变自动重新计算
- 计算值自动 memo
- 不需要 useMemo / selector
(2)、可写派生 atom(读函数 + 写函数)
constcountAtom=atom(0)constdoubleCountAtom=atom(get=>get(countAtom)*2,(get,set,newValue)=>set(countAtom,newValue/2))这是 Jotai 的“神技”,使得:
- 任何派生 atom 都能变成“可写的 computed”
- 非常适合 form 场景
(3)、异步 atom(async)
constuserAtom=atom(async(get)=>{constres=awaitfetch('/api/user')returnres.json()})优点:
- 原生支持 Suspense
- 依赖变化自动重新触发请求
- 不需要 useEffect
这是与 Redux、Recoil、Zustand 最大的差异。
(4)、异步可写原子(异步 action)
constsaveUserAtom=atom(null,async(get,set,user)=>{awaitfetch('/api/save',{method:'POST',body:JSON.stringify(user)})set(userAtom,user)})你可以在 atom 写函数做:
- 请求
- 延迟
- 批处理
- 事务
Jotai 允许你把业务逻辑包成可组合的 atom,不需要额外 action 层。
4、Store 级 API(高级)
(1)、createStore() —— 手动创建独立 store
conststore=createStore()store.get(countAtom)store.set(countAtom,1)用途:
- 在 React 外使用 Jotai(比如 WebWorker、Service、RPC)
- 集成到微前端
- 单元测试
- 时光旅行
(2)、useStore()
暴露内部 store 实例:
conststore=useStore()store.sub(countAtom,()=>console.log('changed'))可用于监听 atom 变化(非常底层)。
5、开发者工具 API
(1)、atom.onMount(set => …) —— 原子的生命周期
当某个 atom 第一次被组件订阅时触发:
constcountAtom=atom(0)countAtom.onMount=(set)=>{constinterval=setInterval(()=>set(c=>c+1),1000)return()=>clearInterval(interval)}这几乎等同于组件级 useEffect,但绑定在 atom 上,而不是组件上。
用于:
- 轮询
- WebSocket
- 实时数据同步
优化:不被订阅就不执行任务
(2)、useAtomCallback()(在回调中读取/写入 atom)
constcallback=useAtomCallback(useCallback(async(get,set)=>{constuser=get(userAtom)set(logAtom,`Fetched:${user.name}`)},[]))为什么它存在?
- 因为 React 回调里不能直接调用 useAtom(),这是 Jotai 的解决方案。
三、Jotai 的思维模式总结(重要 ⭐️)
React 官方精神:小而可组合(small & composable)。
Jotai 把状态拆成最小粒度的原子:
- 每个 atom 就像函数式编程的值容器
- 你可以对 atom 做映射、聚合、派生、链接、组合
- 组件只订阅自己需要的 atom,极致性能
相比:
| 状态库 | 设计理念 |
|---|---|
| Redux | 单一全局 store + reducer/action |
| MobX | 面向对象的响应式模型 |
| Zustand | 一个大对象 + selector |
| Recoil | 类似,但更激进(Jotai 更纯粹) |
| Jotai | 仅由 atom 组成,可以任意组合、派生 |
四、性能细节与最佳实践
- 选择器式订阅:确保组件只订阅需要的 atom(不要在组件里 useAtom 返回一个新对象)。
- 避免在 atom 的 read 中做昂贵同步计算:read 在每次依赖改变时会运行;把重计算缓存到 atom 内或使用 useMemo 在组件侧。
- 拆分大状态:使用实体 map 而不是巨对象或大数组。
- 使用 Provider 的 scope:在大型应用或微前端中使用多个 scope 隔离 store,避免跨域影响。
- 批量更新:write 函数里可以一次性 set 多个 atom,避免多次触发渲染。
五、TypeScript 使用技巧(类型安全)
type User={id:string;name:string};constuserAtom=atom<User|null>(null);constuserNameAtom=atom((get)=>get(userAtom)?.name??'');- 给 atom(initial) 明确类型可以让 useAtom 返回带类型推断的值/写入函数。
- 对于带 write 的 atom,atom<Read, Write> 的写法并不需要显式类型,只要初始类型明确即可。
六、扩展中间件与生态(常见插件)
- jotai/utils:提供 atomFamily, atomWithStorage, atomWithSubscription, atomWithReducer 等实用工厂。
- jotai/immer:在 atom 的 write 中使用 immer 风格的可变写法,代码更简洁。
- jotai/devtools:连接 Redux DevTools,便于调试(注意仅开发环境)。
- jotai/query:与 TanStack Query 的集成,提供 atomWithQuery,把缓存/预取能力补到 atom 层。
- jotai/react(核心):提供 Provider, useAtom, useAtomValue, useSetAtom 等 hooks。
七、SSR(服务端渲染)与 hydration
Jotai 支持 SSR,但需要手动序列化/初始化 atom state ——流程(常见做法):
- 在服务器端创建 atom state、执行所有需要的异步 atom(或通过 renderToStringWithData)。
- 序列化初始 atom 状态为 JSON,放入 HTML。
- 客户端在创建 Provider 时把初始 state 注入(getDefaultStore / Provider initialValues 的方式)。
示例概念:
// server: render -> collect atom values -> window.__JOTAI_PRELOAD__ = {...}// client:constinitial=window.__JOTAI_PRELOAD__;constProvider=createProviderWithInitial(initial);具体实现可参考官方 SSR 示例或社区库(注意 hydration 时避免重复执行副作用)。
八、Persist(本地持久化)
常见做法:
- 使用 atomWithStorage(jotai/utils)实现 localStorage/sessionStorage 持久化。
- 对复杂结构使用 partialize(只存必要字段)并制定 migrate 逻辑,防止版本变更导致数据不兼容。
示例:
import{atomWithStorage}from'jotai/utils';constauthAtom=atomWithStorage('auth',{token:null});九、并发、Suspense 与错误边界(React 18/19)
- Suspense:Jotai 的异步 atom 与 Suspense 自然配合,返回 Promise 的 read 会抛出 Promise。
- Error boundary:异步 atom 的错误会被 ErrorBoundary 捕获(同 React 约定)。
- useAtomValue / useSetAtom:在并发更新中用 useSetAtom 只获取 setter,有助于避免闭包 stale 问题。
十、测试策略
- 单元测试:独立测试 atom read/write 行为,使用 getDefaultStore(或不使用 Provider 的情况下直接调用 atom 的 get)。
- 组件测试:在 Jest + React Testing Library 中 wrap 组件的 render 时传入 Provider 并可注入初始 atom 状态。
- 重置 store:在每个测试用例后清理 atom 状态(避免测试间污染)。
十一、高级技巧与模式
(1)、表单模式(local draft + submit)
- 把表单草稿放在临时 atom(局部),提交时把值写入 server-atom 或触发 action/side-effect。
- Benefit:避免频繁写入全局 state 或 network。
(2)、Optimistic update(乐观更新)
- 在 write 中先更新本地 atom,然后发请求,失败则回滚(在 atom write 中管理回滚快照)。
(3)、Derived cache(缓存派生)
- 对昂贵计算的派生,缓存结果在 atom 中并提供 TTL 或依赖 invalidation(通过另一个 atom 触发刷新)。
(4)、atom factories + dependency injection
- 对于可复用模块(比如 chart store),用工厂创建带入依赖(如 API 客户端)更易测试。
十二、与其它状态库的对比(选择建议)
| 需求 | Jotai | Zustand | Redux Toolkit | Recoil |
|---|---|---|---|---|
| API 简洁度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| 细粒度订阅 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 大型可维护性 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| SSR 支持 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 学习曲线 | 低 | 低 | 高 | 中 |
| 生态 & 工具 | 中 | 中 | 最强 | 小众 |
实务建议:
- 想要最小学习成本且需要按组件订阅:Jotai 很合适。
- 需要严格架构、Timeline、复杂中间件:Redux Toolkit 更稳。
- 需要极简 local store + tiny bundle:Zustand 很好。
十三、常见问题
- 把整个应用状态放到一个大 atom:导致所有组件频繁重渲染。
- Selector 返回新对象:在 read 中返回新对象会导致订阅组件无谓更新。
- 在 atom read 中做副作用:read 应保持纯计算;副作用放到 write 或 useEffect 中。
- 滥用 Suspense:没有正确错误边界会导致用户体验不佳。
- 不处理持久化迁移:localStorage schema 改变必需 migration。
十四、16. 进阶:atomic design 与模块化架构示例(企业级)
项目结构建议:
src/atoms/auth/atoms.ts hooks.ts todo/atoms.ts selectors.ts features/TodoList.tsx TodoItem.tsx pages/providers/JotaiProvider.tsx// 用于 scope / SSR hydration示例 auth/atoms.ts:
exportconsttokenAtom=atomWithStorage('token',null);exportconstuserAtom=atom(async(get)=>{consttoken=get(tokenAtom);if(!token)returnnull;constres=awaitfetch('/api/me',{headers:{Authorization:token}});returnres.json();});