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 直接覆盖
默认值有可能丢字段。
登录后参与评论。