Zustand
Zustand 是一个轻量、简洁的 React 状态管理库。相比 Redux,它没有 Action/Reducer/Provider 等概念,只需定义一个 Store 函数即可使用,非常适合中小型项目的全局状态管理。
核心特点:
- 无需 Provider 包裹,直接 import 使用
- 基于 Hook,语法极简
- 按需订阅,精细控制重渲染
- 内置 DevTools、持久化、Immer 等中间件支持
- 包体积极小(约 1KB)
安装
npm install zustand
基础用法
创建 Store
// src/store/counterStore.ts
import { create } from "zustand"
interface CounterState {
count: number
step: number
increment: () => void
decrement: () => void
incrementBy: (amount: number) => void
setStep: (step: number) => void
reset: () => void
}
export const useCounterStore = create<CounterState>((set, get) => ({
count: 0,
step: 1,
increment: () =>
set((state) => ({ count: state.count + state.step })),
decrement: () =>
set((state) => ({ count: state.count - state.step })),
incrementBy: (amount) =>
set((state) => ({ count: state.count + amount })),
setStep: (step) => set({ step }),
reset: () => set({ count: 0, step: 1 }),
}))
在组件中使用
import { useCounterStore } from "@/store/counterStore"
function Counter() {
// 按需订阅——只有 count 变化时才重渲染,step 变化不触发
const count = useCounterStore((state) => state.count)
const step = useCounterStore((state) => state.step)
const increment = useCounterStore((state) => state.increment)
const decrement = useCounterStore((state) => state.decrement)
const reset = useCounterStore((state) => state.reset)
return (
<div>
<p>计数:{count},步长:{step}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>重置</button>
</div>
)
}
set 与 get
set 用于更新状态,get 用于在 action 内读取当前状态:
export const useStore = create<State>((set, get) => ({
count: 0,
text: "",
// set 合并更新(浅合并,不需要展开其他字段)
setCount: (n) => set({ count: n }),
// set 函数式更新(基于旧值)
increment: () => set((state) => ({ count: state.count + 1 })),
// 第二个参数 true:完全替换 state(慎用)
replaceAll: () => set({ count: 0, text: "reset" }, true),
// get 读取当前值(在异步或复杂逻辑中很有用)
logCount: () => {
const current = get().count
console.log("当前计数:", current)
},
// 在 action 中调用其他 action
doubleIncrement: () => {
get().increment()
get().increment()
},
}))
订阅与重渲染控制
按需订阅(细粒度)
// 只订阅 count,count 变化才重渲染
const count = useStore((state) => state.count)
// 只订阅 actions(actions 引用不变,永不触发重渲染)
const { increment, reset } = useStore((state) => ({
increment: state.increment,
reset: state.reset,
}))
使用 useShallow 订阅多个字段
当需要同时订阅多个字段时,用 useShallow 做浅比较,避免每次都触发重渲染:
import { useShallow } from "zustand/react/shallow"
function UserCard() {
// 没有 useShallow:每次 store 变化都返回新对象引用,导致重渲染
// 有 useShallow:只有 name 或 email 真正变化时才重渲染
const { name, email } = useStore(
useShallow((state) => ({
name: state.user.name,
email: state.user.email,
}))
)
return <div>{name} - {email}</div>
}
// 订阅数组同理
const [count, step] = useStore(
useShallow((state) => [state.count, state.step])
)
在组件外部读取/订阅
// 在非组件代码(如工具函数、路由守卫)中访问 store
const currentCount = useCounterStore.getState().count
// 在非组件代码中修改状态
useCounterStore.setState({ count: 100 })
useCounterStore.setState((state) => ({ count: state.count + 1 }))
// 订阅 state 变化(返回取消订阅函数)
const unsubscribe = useCounterStore.subscribe(
(state) => console.log("count changed:", state.count)
)
unsubscribe() // 取消订阅
异步 Action
Zustand 的 action 天然支持异步,无需任何特殊处理:
// src/store/userStore.ts
import { create } from "zustand"
interface User {
id: number
name: string
email: string
}
interface UserState {
users: User[]
currentUser: User | null
status: "idle" | "loading" | "error"
error: string | null
fetchUsers: () => Promise<void>
fetchUserById: (id: number) => Promise<void>
createUser: (data: Omit<User, "id">) => Promise<User>
deleteUser: (id: number) => Promise<void>
clearError: () => void
}
export const useUserStore = create<UserState>((set, get) => ({
users: [],
currentUser: null,
status: "idle",
error: null,
fetchUsers: async () => {
set({ status: "loading", error: null })
try {
const res = await fetch("/api/users")
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const users: User[] = await res.json()
set({ users, status: "idle" })
} catch (err) {
set({ error: (err as Error).message, status: "error" })
}
},
fetchUserById: async (id) => {
set({ status: "loading" })
try {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error("用户不存在")
const user: User = await res.json()
set({ currentUser: user, status: "idle" })
} catch (err) {
set({ error: (err as Error).message, status: "error" })
}
},
createUser: async (data) => {
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
const newUser: User = await res.json()
// 直接更新列表,无需重新请求
set((state) => ({ users: [...state.users, newUser] }))
return newUser
},
deleteUser: async (id) => {
await fetch(`/api/users/${id}`, { method: "DELETE" })
set((state) => ({
users: state.users.filter((u) => u.id !== id),
}))
},
clearError: () => set({ error: null, status: "idle" }),
}))
function UserList() {
const users = useUserStore((state) => state.users)
const status = useUserStore((state) => state.status)
const fetchUsers = useUserStore((state) => state.fetchUsers)
const deleteUser = useUserStore((state) => state.deleteUser)
useEffect(() => {
fetchUsers()
}, [])
if (status === "loading") return <p>加载中...</p>
return (
<ul>
{users.map((u) => (
<li key={u.id}>
{u.name}
<button onClick={() => deleteUser(u.id)}>删除</button>
</li>
))}
</ul>
)
}
中间件
devtools:Redux DevTools 支持
import { create } from "zustand"
import { devtools } from "zustand/middleware"
export const useStore = create<State>()(
devtools(
(set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 }), false, "increment"),
// ^^^^ ^^^^^^^^^^^
// false = 合并 action 名称(DevTools 显示)
}),
{ name: "CounterStore", enabled: process.env.NODE_ENV === "development" }
)
)
persist:状态持久化
import { create } from "zustand"
import { persist, createJSONStorage } from "zustand/middleware"
interface SettingsState {
theme: "light" | "dark"
language: string
fontSize: number
setTheme: (theme: "light" | "dark") => void
setLanguage: (lang: string) => void
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: "light",
language: "zh",
fontSize: 16,
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}),
{
name: "app-settings", // localStorage key
storage: createJSONStorage(() => localStorage), // 默认即 localStorage
// 只持久化部分字段
partialize: (state) => ({
theme: state.theme,
language: state.language,
}),
// 版本迁移
version: 2,
migrate: (persistedState: any, version) => {
if (version === 1) {
// 从 v1 迁移到 v2:字段重命名
persistedState.language = persistedState.lang ?? "zh"
delete persistedState.lang
}
return persistedState as SettingsState
},
}
)
)
// 使用 sessionStorage
export const useSessionStore = create<State>()(
persist(
(set) => ({ ... }),
{
name: "session-data",
storage: createJSONStorage(() => sessionStorage),
}
)
)
immer:直接修改嵌套状态
import { create } from "zustand"
import { immer } from "zustand/middleware/immer"
interface TodoState {
todos: { id: number; text: string; done: boolean }[]
addTodo: (text: string) => void
toggleTodo: (id: number) => void
removeTodo: (id: number) => void
editTodo: (id: number, text: string) => void
}
export const useTodoStore = create<TodoState>()(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
// 直接 push,immer 处理不可变性
state.todos.push({ id: Date.now(), text, done: false })
}),
toggleTodo: (id) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id)
if (todo) todo.done = !todo.done // 直接修改
}),
removeTodo: (id) =>
set((state) => {
const index = state.todos.findIndex((t) => t.id === id)
if (index !== -1) state.todos.splice(index, 1)
}),
editTodo: (id, text) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id)
if (todo) todo.text = text
}),
}))
)
组合多个中间件
import { create } from "zustand"
import { devtools, persist, immer } from "zustand/middleware"
// 中间件从外到内包裹,顺序:devtools > persist > immer
export const useStore = create<State>()(
devtools(
persist(
immer((set, get) => ({
// store 定义
})),
{ name: "my-store" }
),
{ name: "MyStore" }
)
)
拆分与组合 Store
方式一:多个独立 Store(推荐)
// 职责单一,按功能拆分
export const useAuthStore = create<AuthState>(...)
export const useUIStore = create<UIState>(...)
export const useProductStore = create<ProductState>(...)
方式二:Slice 模式(大型单一 Store)
// src/store/slices/cartSlice.ts
import type { StateCreator } from "zustand"
interface CartSlice {
items: CartItem[]
total: number
addItem: (item: CartItem) => void
removeItem: (id: number) => void
clearCart: () => void
}
export const createCartSlice: StateCreator<
CartSlice & UserSlice, // 整体 Store 类型(可访问其他 slice)
[],
[],
CartSlice
> = (set, get) => ({
items: [],
total: 0,
addItem: (item) =>
set((state) => {
const next = [...state.items, item]
return {
items: next,
total: next.reduce((sum, i) => sum + i.price * i.qty, 0),
}
}),
removeItem: (id) =>
set((state) => {
const next = state.items.filter((i) => i.id !== id)
return {
items: next,
total: next.reduce((sum, i) => sum + i.price * i.qty, 0),
}
}),
clearCart: () => set({ items: [], total: 0 }),
})
// src/store/slices/userSlice.ts
import type { StateCreator } from "zustand"
interface UserSlice {
user: User | null
setUser: (user: User | null) => void
}
export const createUserSlice: StateCreator<
CartSlice & UserSlice,
[],
[],
UserSlice
> = (set) => ({
user: null,
setUser: (user) => set({ user }),
})
// src/store/index.ts
import { create } from "zustand"
import { createCartSlice } from "./slices/cartSlice"
import { createUserSlice } from "./slices/userSlice"
export const useStore = create<CartSlice & UserSlice>()((...args) => ({
...createCartSlice(...args),
...createUserSlice(...args),
}))
// 可以为每个 slice 导出专用 hook,保持使用侧简洁
export const useCart = () =>
useStore(
useShallow((s) => ({
items: s.items,
total: s.total,
addItem: s.addItem,
removeItem: s.removeItem,
clearCart: s.clearCart,
}))
)
派生状态
在 selector 中计算派生值,等同于 Redux 的 createSelector:
interface TodoState {
todos: { id: number; text: string; done: boolean }[]
filter: "all" | "active" | "completed"
}
// 在组件中用 selector 计算派生值
function TodoStats() {
const total = useTodoStore((s) => s.todos.length)
const completed = useTodoStore((s) => s.todos.filter((t) => t.done).length)
const active = useTodoStore((s) => s.todos.filter((t) => !t.done).length)
return <p>全部 {total} / 已完成 {completed} / 进行中 {active}</p>
}
// 过滤列表
function TodoList() {
const filtered = useTodoStore((s) => {
if (s.filter === "active") return s.todos.filter((t) => !t.done)
if (s.filter === "completed") return s.todos.filter((t) => t.done)
return s.todos
})
return <ul>{filtered.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
}
对于开销较大的派生计算,配合 useMemo 或直接使用 zustand-computed:
import { useMemo } from "react"
function ExpensiveList() {
const todos = useTodoStore((s) => s.todos)
const filter = useTodoStore((s) => s.filter)
// 只在 todos 或 filter 变化时重新计算
const filtered = useMemo(() => {
return todos.filter((t) => {
if (filter === "active") return !t.done
if (filter === "completed") return t.done
return true
})
}, [todos, filter])
return <ul>{filtered.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
}
在 React 外使用
Zustand store 是独立的,不依赖 React,可以在任何地方使用:
// 路由守卫
function requireAuth() {
const { user } = useAuthStore.getState()
if (!user) {
window.location.href = "/login"
return false
}
return true
}
// axios 拦截器
axios.interceptors.request.use((config) => {
const token = useAuthStore.getState().token
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// 订阅特定字段变化(按需执行副作用)
const unsubscribe = useAuthStore.subscribe(
(state) => state.user, // 选取要监听的字段
(user, prevUser) => { // user 变化时执行
if (!user && prevUser) {
console.log("用户已登出")
clearSensitiveData()
}
}
)
测试
// 测试前重置 store
import { act } from "@testing-library/react"
import { useCounterStore } from "@/store/counterStore"
beforeEach(() => {
useCounterStore.setState({ count: 0, step: 1 })
})
test("increment 增加 step", () => {
const { increment, setStep } = useCounterStore.getState()
act(() => setStep(5))
act(() => increment())
expect(useCounterStore.getState().count).toBe(5)
})
test("reset 清零", () => {
useCounterStore.setState({ count: 100 })
act(() => useCounterStore.getState().reset())
expect(useCounterStore.getState().count).toBe(0)
})
完整示例:购物车
// src/store/cartStore.ts
import { create } from "zustand"
import { persist, devtools } from "zustand/middleware"
import { immer } from "zustand/middleware/immer"
import { useShallow } from "zustand/react/shallow"
interface CartItem {
id: number
name: string
price: number
qty: number
}
interface CartState {
items: CartItem[]
// 派生值(通过 selector 计算,不存在 state 中)
addItem: (item: Omit<CartItem, "qty">) => void
removeItem: (id: number) => void
updateQty: (id: number, qty: number) => void
clearCart: () => void
}
export const useCartStore = create<CartState>()(
devtools(
persist(
immer((set) => ({
items: [],
addItem: (product) =>
set((state) => {
const existing = state.items.find((i) => i.id === product.id)
if (existing) {
existing.qty += 1
} else {
state.items.push({ ...product, qty: 1 })
}
}),
removeItem: (id) =>
set((state) => {
const index = state.items.findIndex((i) => i.id === id)
if (index !== -1) state.items.splice(index, 1)
}),
updateQty: (id, qty) =>
set((state) => {
const item = state.items.find((i) => i.id === id)
if (item) {
if (qty <= 0) {
state.items = state.items.filter((i) => i.id !== id)
} else {
item.qty = qty
}
}
}),
clearCart: () => set({ items: [] }),
})),
{ name: "cart", partialize: (s) => ({ items: s.items }) }
),
{ name: "CartStore" }
)
)
// 封装派生 selector
export const useCartItems = () => useCartStore((s) => s.items)
export const useCartCount = () => useCartStore((s) => s.items.reduce((n, i) => n + i.qty, 0))
export const useCartTotal = () => useCartStore((s) => s.items.reduce((n, i) => n + i.price * i.qty, 0))
export const useCartActions = () =>
useCartStore(
useShallow((s) => ({
addItem: s.addItem,
removeItem: s.removeItem,
updateQty: s.updateQty,
clearCart: s.clearCart,
}))
)
// 使用
function CartSummary() {
const items = useCartItems()
const count = useCartCount()
const total = useCartTotal()
const { removeItem, updateQty, clearCart } = useCartActions()
return (
<div>
<h2>购物车({count} 件)</h2>
{items.map((item) => (
<div key={item.id}>
<span>{item.name}</span>
<input
type="number"
value={item.qty}
onChange={(e) => updateQty(item.id, Number(e.target.value))}
/>
<span>¥{(item.price * item.qty).toFixed(2)}</span>
<button onClick={() => removeItem(item.id)}>移除</button>
</div>
))}
<p>合计:¥{total.toFixed(2)}</p>
<button onClick={clearCart}>清空购物车</button>
</div>
)
}
与 Redux 对比
| Zustand | Redux Toolkit | |
|---|---|---|
| 样板代码 | 极少 | 较多(Slice/Store/Provider) |
| 需要 Provider | 不需要 | 需要 <Provider> |
| 异步处理 | 原生支持,直接 async/await | 需要 createAsyncThunk |
| 中间件 | 内置 devtools/persist/immer | 需手动配置 |
| DevTools | 支持 | 支持(功能更完整) |
| 数据请求缓存 | 无内置(配合 React Query) | RTK Query |
| 适用规模 | 中小型项目 | 大型复杂项目 |
| 学习成本 | 低 | 中 |