起因
复杂表单 / 多级嵌套组件共享 state 时,Zustand 的"一个 store 多字段"
还是会让"用 fieldA 的组件" 因为 fieldB 改而 re-render(除非每字段
单独 selector)。
Jotai 思路完全不同:每个状态是一个独立的 atom。组件只订阅它真用到
的 atom,atom 变才重渲。天然细粒度。
解决方案
装
npm i jotai
第一个 atom
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
function Counter() {
const [count, setCount] = useAtom(countAtom)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
function Display() {
const [count] = useAtom(countAtom)
return <span>{count}</span>
}
useAtom 跟 useState 几乎一样的 API。
多个组件共享同一 atom 时自动同步。
不需要 Provider(默认全局):
function App() {
return (
<>
<Counter />
<Display />
</>
)
}
两个组件用同一 atom,互相同步。
Derived atom(计算属性)
const countAtom = atom(0)
const doubledAtom = atom((get) => get(countAtom) * 2)
function Doubled() {
const [doubled] = useAtom(doubledAtom)
return <span>{doubled}</span>
}
doubledAtom 自动跟 countAtom 关联:count 变 → doubled 重算 →
订阅 doubled 的组件重渲。
Async atom
const userIdAtom = atom('alice')
const userAtom = atom(async (get) => {
const id = get(userIdAtom)
const r = await fetch(`/api/users/${id}`)
return r.json()
})
function UserCard() {
const [user] = useAtom(userAtom)
return <div>{user.name}</div>
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserCard />
</Suspense>
)
}
异步 atom 自动用 React Suspense。改 userIdAtom 触发 refetch。
Atom family(动态创建 atoms)
每个 user id 一个 atom:
import { atomFamily } from 'jotai/utils'
const userAtomFamily = atomFamily((id: string) =>
atom(async () => fetch(`/api/users/${id}`).then(r => r.json()))
)
function UserCard({ id }: { id: string }) {
const [user] = useAtom(userAtomFamily(id))
return <div>{user.name}</div>
}
每个 id 独立 atom;不同 UserCard 不会互相影响。
写组合:写一个 atom 触发多个修改
const firstNameAtom = atom('')
const lastNameAtom = atom('')
const updateNameAtom = atom(
null, // read function(这里没读)
(get, set, fullName: string) => {
const [f, l] = fullName.split(' ')
set(firstNameAtom, f)
set(lastNameAtom, l)
}
)
function Form() {
const [, setName] = useAtom(updateNameAtom)
return <input onChange={e => setName(e.target.value)} />
}
action / mutation 风格。
持久化
import { atomWithStorage } from 'jotai/utils'
const themeAtom = atomWithStorage('theme', 'light')
function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom)
return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
{theme}
</button>
}
自动 localStorage。
跟 Zustand 比
| Jotai | Zustand | |
|---|---|---|
| 粒度 | atom(极细) | store(粗,需 selector) |
| API 风格 | 函数式 hook | mutate / function |
| 学习曲线 | 中(atom 思维转换) | 低 |
| 大组件树 | 优秀(自动只更新订阅者) | 需精确 selector |
| 异步 | 一等公民 + Suspense | 手写 |
| 复杂派生 | 自然 | 需 useMemo 等 |
| Devtools | 有 | 有 |
适合 Jotai 的场景:
- 复杂表单(每字段独立 atom)
- 大量派生状态
- 需要 Suspense 异步
适合 Zustand:
- 简单全局 store(用户 auth / theme)
- 倾向 imperative API
实战:表单 with field-level state
import { atom, useAtom } from 'jotai'
const emailAtom = atom('')
const emailErrorAtom = atom((get) => {
const e = get(emailAtom)
if (!e) return null
if (!/@/.test(e)) return '邮箱格式错'
return null
})
const passwordAtom = atom('')
const passwordErrorAtom = atom((get) => {
const p = get(passwordAtom)
if (p.length < 8) return '密码至少 8 位'
return null
})
const canSubmitAtom = atom((get) =>
get(emailErrorAtom) === null && get(passwordErrorAtom) === null
&& get(emailAtom) && get(passwordAtom)
)
function EmailField() {
const [email, setEmail] = useAtom(emailAtom)
const [error] = useAtom(emailErrorAtom)
return (
<div>
<input value={email} onChange={e => setEmail(e.target.value)} />
{error && <p>{error}</p>}
</div>
)
}
// PasswordField 类似
function SubmitButton() {
const [canSubmit] = useAtom(canSubmitAtom)
return <button disabled={!canSubmit}>提交</button>
}
每个字段独立 atom + 独立 error atom + 派生 canSubmit。
只有"我用到的" 重渲。
对比 react-hook-form:
- RHF 更成熟 + 更多功能(registration / validation chain)
- Jotai 概念上更轻 + 不限于表单
复杂表单仍推荐 RHF;中等表单 + 跨组件共享 Jotai 更灵活。
与 React Query 集成
import { atomWithQuery } from 'jotai-tanstack-query'
const userAtom = atomWithQuery((get) => ({
queryKey: ['user', get(userIdAtom)],
queryFn: async ({ queryKey: [, id] }) =>
fetch(`/api/users/${id}`).then(r => r.json()),
}))
function UserCard() {
const [{ data, isPending }] = useAtom(userAtom)
if (isPending) return <Spinner />
return <div>{data.name}</div>
}
React Query 的 cache + Jotai 的 atom 组合 = 服务端状态 + 客户端
state 一致管理。
调试:Jotai Devtools
import { useAtomDevtools } from 'jotai-devtools'
function DebugAll() {
useAtomDevtools(countAtom, { name: 'count' })
useAtomDevtools(userAtom, { name: 'user' })
return null
}
Redux DevTools 显示所有 atom 变化时间线 + 当前值。
性能注意
Jotai 极细粒度 = 每个 atom 一份 React subscription。
几千个 atom + 大量更新 时 React scheduler 压力大。
千级 atom 场景考虑:
- 用
atomFamily而非手写百份 atom - 把不需要响应的数据放普通对象 / Map
- 必要时用 Jotai 内部 store 做 batched update
何时不用 Jotai
- 全局只有几个简单 state(auth + theme)→ Zustand 足够
- 已经深度用 Redux Toolkit → 切换成本大
- 团队不熟函数式 → 学习曲线一道槛
踩过的坑
-
atom 在组件外创建:放函数体内每次 render 新 atom → state 重置。
atom 一定在 module top-level 创建。 -
派生 atom 依赖循环:A 依赖 B,B 依赖 A → 无限循环 stack overflow。
设计时检查依赖图无环。 -
async atom + Suspense:默认 fetch 失败抛 ErrorBoundary。
不想用 Suspense 写loadable(asyncAtom)退化到 loading state pattern。 -
atom 大对象:整个对象一变所有订阅者重渲。改为多 atom 细分 +
selector 派生。 -
SSR / Next.js:Jotai SSR 需要
Provider隔离 request-level
state,不能全局。注意配 hydration。
登录后参与评论。