zustand vs jotai:原子化状态管理对比与选型指南

HTMLPAGE 团队
14 分钟阅读

对比两个轻量级状态管理方案的核心特性、API 设计、适用场景,帮助团队在 Pinia/Redux 之外做出明智的选择。

#State Management #zustand #jotai #React #Architecture

zustand vs jotai:原子化状态管理对比与选型指南

当项目从简单的 React hooks 状态管理升级到需要更系统的方案时,很多团队的第一反应是 Redux。但 Redux 的学习曲线和样板代码量往往让人望而却步。

这两年出现了两个轻量级的替代品:

  • zustand:简化的、类似 Pinia 的单一 store
  • jotai:原子化设计,类似 Recoil 但更轻

它们都主打"轻量级",但设计哲学截然不同。选对了,能大幅降低复杂度;选错了,可能在后期重构。


1. 核心哲学对比

维度zustandjotai
核心概念单一 store(集中式)原子集合(分散式)
状态组织一个大对象多个小原子
更新方式immer 风格或 setStatesetter 函数
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(够用且更简单)

9. 内链与深入学习