zustand vs jotai:原子化状态管理对比与选型指南
当项目从简单的 React hooks 状态管理升级到需要更系统的方案时,很多团队的第一反应是 Redux。但 Redux 的学习曲线和样板代码量往往让人望而却步。
这两年出现了两个轻量级的替代品:
- zustand:简化的、类似 Pinia 的单一 store
- jotai:原子化设计,类似 Recoil 但更轻
它们都主打"轻量级",但设计哲学截然不同。选对了,能大幅降低复杂度;选错了,可能在后期重构。
1. 核心哲学对比
| 维度 | zustand | jotai |
|---|---|---|
| 核心概念 | 单一 store(集中式) | 原子集合(分散式) |
| 状态组织 | 一个大对象 | 多个小原子 |
| 更新方式 | immer 风格或 setState | setter 函数 |
| Bundle 大小 | ~2KB | ~3KB |
| 学习曲线 | 极低(就像 Pinia) | 中等(需要理解"原子"概念) |
| DevTools 集成 | 有,比较好 | 有,但不如 zustand |
2. zustand:简洁的集中式管理
设计理念:把状态管理做得极简,就像用 Pinia 一样直观。
2.1 基础用法
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer' // 可选
// 定义 store
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
// action
increment: () => set((state) => ({ count: state.count + 1 })),
// 或用 immer 中间件简化写法
incrementByImmer: () => set(
(state) => { state.count += 1 }
)
}))
// 在组件里用
function Counter() {
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
return <button onClick={increment}>{count}</button>
}
2.2 高级特性
export const useAppStore = create<AppState>((set, get) => ({
user: null,
notifications: [],
// 用 get 访问当前状态
logout: () => {
const { user } = get()
if (user) {
api.notifyId(user.id)
}
set({ user: null })
},
// 跨 store 通信
notify: (message) => {
set((state) => ({
notifications: [...state.notifications, message]
}))
}
}))
// 订阅状态变化
useCounterStore.subscribe(
(state) => state.count,
(count) => console.log('Count changed:', count)
)
// 重置
useCounterStore.setState({ count: 0 })
2.3 zustand 的优势
- API 最直观:本质上就是
useState的升级版 - 跨 store 通信很简单:直接 import 调用
- DevTools 做得好:时间旅行调试、action 录像
- 性能优秀:订阅粒度细,不需要的重新渲染少
- 中间件生态:immer、persist、devtools 等开箱即用
2.4 zustand 的限制
- 无天生的异步处理:需要手动管理 loading 状态
- 大型应用扩展性:如果 store 很大,缺少"分模块"的机制
3. jotai:原子化的分散式管理
设计理念:把状态分解成最小单位(原子),像 Recoil 一样灵活。
3.1 基础用法
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
// 定义原子
export const countAtom = atom(0)
export const nameAtom = atom('Alice')
// 派生原子
export const descriptionAtom = atom((get) => {
const count = get(countAtom)
const name = get(nameAtom)
return `${name} has ${count} items`
})
// 异步原子
export const userAtom = atom(async () => {
return fetch('/api/user').then(r => r.json())
})
// 在组件里用
function App() {
// 读写原子
const [count, setCount] = useAtom(countAtom)
// 只读
const description = useAtomValue(descriptionAtom)
// 只写
const setName = useSetAtom(nameAtom)
return (
<div>
<p>{description}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={() => setName('Bob')}>Change name</button>
</div>
)
}
3.2 高级特性
// 原子群(同时更新多个)
export const userAtom = atom((get) => {
const id = get(idAtom)
const name = get(nameAtom)
return { id, name }
})
// 可写派生原子
export const doubleCountAtom = atom(
(get) => get(countAtom) * 2,
(get, set, newValue) => {
set(countAtom, newValue / 2)
}
)
// 异步更新
export const fetchUserAtom = atom(null, async (get, set, userId) => {
const user = await api.getUser(userId)
set(userAtom, user)
})
3.3 jotai 的优势
- 细粒度反应性:只有关联的原子改变才重新渲染
- 天生适合异步:内置对 Suspense 和 Promise 的支持
- 天生组合性:派生原子自动依赖追踪(类似 computed)
- 更小的包体积
- React 新特性友好:与 Suspense、Server Components 配合得好
3.4 jotai 的限制
- 学习曲线更陡:需要理解"原子"和"派生"的概念
- 调试复杂性:原子太多时,数据流可能难以追踪
- DevTools 支持不如 zustand
- 团队协作:如果团队习惯"集中式"思维,适应会慢
4. 场景选择指南
使用 zustand 如果:
✅ 应用状态结构清晰,核心状态集中
✅ 团队已有 Pinia 或 Vuex 经验(思维方式一致)
✅ 需要强大的 DevTools 支持
✅ 更新逻辑相对复杂(需要 immer)
✅ 对 bundle 大小敏感
典型场景:管理后台、电商平台、内容编辑器
使用 jotai 如果:
✅ 状态细粒度,需要精确控制渲染
✅ 有大量异步操作(API 调用)
✅ 使用 React Suspense、Server Components
✅ 团队更偏函数式编程思维
✅ 需要原子级别的状态共享(比如表单的每个字段)
典型场景:实时协作应用、复杂表单、仪表板
5. 实战对比:以"用户列表 + 搜索"为例
zustand 方案
interface ListState {
items: User[]
search: string
loading: boolean
error: string | null
setSearch: (s: string) => void
fetchItems: () => Promise<void>
reset: () => void
}
export const useListStore = create<ListState>()((set, get) => ({
items: [],
search: '',
loading: false,
error: null,
setSearch: (search) => set({ search }),
fetchItems: async () => {
set({ loading: true, error: null })
try {
const data = await api.search(get().search)
set({ items: data })
} catch (err) {
set({ error: err.message })
} finally {
set({ loading: false })
}
},
reset: () => set({
items: [],
search: '',
loading: false,
error: null
})
}))
// 使用
function ListPage() {
const search = useListStore(s => s.search)
const setSearch = useListStore(s => s.setSearch)
const items = useListStore(s => s.items)
const loading = useListStore(s => s.loading)
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<SearchButton onClick={useListStore(s => s.fetchItems)} />
{loading ? <Spinner /> : <List items={items} />}
</div>
)
}
jotai 方案
// 基础原子
export const searchAtom = atom('')
export const itemsAtom = atom<User[]>([])
export const loadingAtom = atom(false)
export const errorAtom = atom<string | null>(null)
// 异步原子(自动做 loading)
export const fetchItemsAtom = atom(
null,
async (get, set) => {
set(loadingAtom, true)
try {
const search = get(searchAtom)
const data = await api.search(search)
set(itemsAtom, data)
set(errorAtom, null)
} catch (err) {
set(errorAtom, err.message)
} finally {
set(loadingAtom, false)
}
}
)
// 使用
function ListPage() {
const [search, setSearch] = useAtom(searchAtom)
const items = useAtomValue(itemsAtom)
const loading = useAtomValue(loadingAtom)
const fetchItems = useSetAtom(fetchItemsAtom)
return (
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<SearchButton onClick={() => fetchItems()} />
{loading ? <Spinner /> : <List items={items} />}
</div>
)
}
对比观察:
- zustand:更像传统命令式,逻辑集中在 store
- jotai:更声明式,状态分散,但让每个原子职责清晰
6. 性能考量
zustand:
- 每次 set() 都会重新渲染订阅该 store 的所有选择器
- 但选择器本身很快(基于引用比较)
jotai:
- 只重新渲染用过该原子的组件
- 原子级别的更新,粒度更细
结论:对大多数应用,两者性能差异不明显。jotai 在"高频更新不同原子"场景能做得更好,zustand 在"大量组件共用同一个 store"时更简洁。
7. 迁移与混用
如果项目现有 Redux 或 Pinia,需要迁移吗?
建议:不要为了迁移而迁移。
- zustand 和 jotai 都支持和原有方案并存
- 新功能用新方案,旧功能逐步迁移
- 避免一次全量替换(风险高,收益小)
8. 总结与决策树
应用规模?
├─ 小(<5个页面)
│ └─ 用 zustand(极简化) 或 React hooks + localStorage
├─ 中等(5-50 页面)
│ ├─ 状态相对集中 → zustand
│ └─ 状态分散,异步多 → jotai
└─ 大型(>50 页面)
└─ zustand(配合模块化) 或 jotai(原子化天生扩展好)
团队背景?
├─ Vue/Pinia 经验 → zustand(思维平移成本低)
└─ Recoil/函数式 → jotai(概念一致)
性能critical?
├─ 是 → jotai(细粒度更优)
└─ 否 → zustand(够用且更简单)


