Zustand:用 4 KB 替代 Redux Toolkit 做 React 状态管理

Redux Toolkit 适合大型项目,对小项目却有大量样板。Zustand 是
Pmndrs(与 Three.js / Jotai 同作者)的极简方案,2024 后越来越流行。

安装

npm i zustand

最小 store

// stores/counter.ts
import { create } from 'zustand'

interface State {
  count: number
  inc: () => void
  reset: () => void
}

export const useCounter = create<State>((set) => ({
  count: 0,
  inc: () => set(s => ({ count: s.count + 1 })),
  reset: () => set({ count: 0 }),
}))
function Counter() {
  const count = useCounter(s => s.count)
  const inc = useCounter(s => s.inc)
  return <button onClick={inc}>{count}</button>
}

无 Provider、无 reducer、无 action type。selector 写法天然做了 memoization
(只有 selector 返回值变了才重渲染)。

persist middleware

import { persist, createJSONStorage } from 'zustand/middleware'

export const useSettings = create(
  persist<Settings>(
    (set) => ({
      theme: 'light',
      setTheme: (t) => set({ theme: t }),
    }),
    {
      name: 'settings',
      storage: createJSONStorage(() => localStorage),
      version: 1,
    }
  )
)

刷新页面状态自动还原。

immer middleware(写嵌套结构方便)

import { immer } from 'zustand/middleware/immer'

const useTodos = create<TodoState>()(
  immer((set) => ({
    todos: [],
    addTodo: (text) => set(s => { s.todos.push({ id: Date.now(), text }) }),
    toggle: (id) => set(s => {
      const t = s.todos.find(t => t.id === id)
      if (t) t.done = !t.done
    }),
  }))
)

直接 mutate(看起来),immer 内部生成 immutable 更新。

多 store vs 单 store

Zustand 鼓励"多个小 store",每个独立:

useAuth()      // 登录状态
useTheme()     // 主题
useTodos()     // 待办列表

而不是 Redux 那种"一个 root reducer"。
小 store 更容易代码分割 / 测试 / 删除。

异步 action

const useUser = create<UserState>((set) => ({
  user: null,
  loading: false,
  async fetchUser(id: string) {
    set({ loading: true })
    try {
      const r = await fetch(`/api/users/${id}`)
      set({ user: await r.json(), loading: false })
    } catch (e) {
      set({ loading: false })
    }
  },
}))

异步逻辑就是普通 async 函数,无需 thunk / saga。

选择多个字段时避免 re-render

// ❌ 每次新对象 → 总是重渲
const { name, email } = useUser(s => ({ name: s.name, email: s.email }))

// ✅ 用 shallow 比较
import { shallow } from 'zustand/shallow'
const { name, email } = useUser(s => ({ name: s.name, email: s.email }), shallow)

// 或者分别 select
const name = useUser(s => s.name)
const email = useUser(s => s.email)

getState / setState(store 之外用)

useAuth.getState().logout()
useAuth.setState({ user: null })

// 订阅外部
const unsub = useAuth.subscribe(
  (state) => state.user,
  (user) => console.log('user changed', user)
)

适合在路由 hook、worker 等非 React 上下文里用。

Devtools

import { devtools } from 'zustand/middleware'

const useStore = create(devtools<State>(
  (set) => ({ ... }),
  { name: 'MyStore' }
))

Redux DevTools 扩展能看到时间线 + state diff。

与 React Server Components

Zustand 是 client-side 状态。RSC 里直接用 fetch;只在交互组件("use client")
里用 store。

测试

import { act } from 'react'

it('inc works', () => {
  const { inc, count } = useCounter.getState()
  expect(count).toBe(0)
  act(() => inc())
  expect(useCounter.getState().count).toBe(1)
})

// 测后重置:
beforeEach(() => useCounter.setState({ count: 0 }))

vs Jotai / Recoil

  • Zustand:单值存储 + selector,类似 mini Redux
  • Jotai:atomic state,类似 SolidJS signal
  • Recoil:Facebook 出的 atom 风格,已停止维护

中型项目用 Zustand 最实用;大量原子化派生状态用 Jotai。

踩过的坑

  • selector 返回新对象 → 总是重渲。要么 shallow 要么拆字段 select。
  • store 闭包了旧值:set 用函数形式 set(s => ...) 而不是 set({})
  • store 不要循环依赖(A store 里 select B store)。把跨 store 派生
    搬到组件层。
  • persist 跨大版本:手动写 migrate 函数;不写则旧 state 直接覆盖
    默认值有可能丢字段。
精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。