1. 为什么选 Zustand?
| 维度 | Redux Toolkit | Zustand |
|---|---|---|
| 包体积 | ~20 kB | ~2 kB |
| 样板代码 | 有 slice、action、reducer | 几乎 0 样板 |
| 学习曲线 | 中等 | 极低 |
| 异步 | 需写 thunk / RTK Query | 直接 async/await |
| 渲染优化 | 需手动 shallowEqual | 内置 selector |
一句话:Zustand 就是 React 世界里最轻、最灵活、最贴近原生语法的全局状态库。
2. 安装 & 项目初始化
npm i zustand
# 如果你用 React 18
npm i zustand@latest
3. 核心概念 30 秒速通
- store:一个函数,返回一个对象(状态 + 方法)。
- hook:
create()返回的自定义 Hook,组件里直接useStore()。 - selector:在 Hook 里传入函数,精准订阅局部状态,避免多余渲染。
4. 实战 1:计数器(最小可运行示例)
// src/stores/counterStore.ts
import { create } from 'zustand'
interface CounterState {
count: number
inc: () => void
dec: () => void
reset: () => void
}
export const useCounter = create<CounterState>((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
// src/components/Counter.tsx
import { useCounter } from '../stores/counterStore'
export default function Counter() {
const { count, inc, dec, reset } = useCounter()
return (
<div>
<h1>{count}</h1>
<button onClick={inc}>+</button>
<button onClick={dec}>-</button>
<button onClick={reset}>Reset</button>
</div>
)
}
5. 实战 2:异步请求 + 乐观更新
需求:点击按钮拉取用户数据,加载中显示骨架屏,失败可重试。
// src/stores/userStore.ts
import { create } from 'zustand'
interface User {
id: number
name: string
}
interface UserState {
user: User | null
loading: boolean
error: string | null
fetchUser: (id: number) => Promise<void>
}
export const useUser = create<UserState>((set) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id: number) => {
set({ loading: true, error: null })
try {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
const user = (await res.json()) as User
set({ user, loading: false })
} catch (e: any) {
set({ error: e.message, loading: false })
}
},
}))
// src/components/UserCard.tsx
import { useUser } from '../stores/userStore'
export default function UserCard({ id }: { id: number }) {
const { user, loading, error, fetchUser } = useUser()
return (
<div>
{loading && <p>Loading...</p>}
{error && (
<p>
{error} <button onClick={() => fetchUser(id)}>Retry</button>
</p>
)}
{user && (
<div>
<h2>{user.name}</h2>
<button onClick={() => fetchUser(id)}>Refresh</button>
</div>
)}
</div>
)
}
6. 实战 3:跨组件共享购物车(含持久化)
需求:
- 商品列表页点击“加入购物车”。
- 购物车浮层实时展示数量、总价。
- 刷新页面后数据不丢失。
6.1 定义 store
// src/stores/cartStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export interface CartItem {
id: number
name: string
price: number
qty: number
}
interface CartState {
items: CartItem[]
addItem: (product: Omit<CartItem, 'qty'>) => void
removeItem: (id: number) => void
clear: () => void
totalQty: () => number
totalPrice: () => number
}
export const useCart = create<CartState>()(
persist(
(set, get) => ({
items: [],
addItem: (product) =>
set((state) => {
const exist = state.items.find((i) => i.id === product.id)
if (exist) {
return {
items: state.items.map((i) =>
i.id === product.id ? { ...i, qty: i.qty + 1 } : i
),
}
}
return { items: [...state.items, { ...product, qty: 1 }] }
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
clear: () => set({ items: [] }),
totalQty: () => get().items.reduce((sum, i) => sum + i.qty, 0),
totalPrice: () =>
get().items.reduce((sum, i) => sum + i.price * i.qty, 0),
}),
{ name: 'cart-storage' } // localStorage key
)
)
6.2 商品列表
// src/components/ProductList.tsx
import { useCart } from '../stores/cartStore'
const products = [
{ id: 1, name: 'iPhone 16', price: 7999 },
{ id: 2, name: 'MacBook Pro', price: 14999 },
]
export default function ProductList() {
const addItem = useCart((s) => s.addItem)
return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} - ¥{p.price}
<button onClick={() => addItem(p)}>Add</button>
</li>
))}
</ul>
)
}
6.3 购物车浮层
// src/components/CartFloat.tsx
import { useCart } from '../stores/cartStore'
export default function CartFloat() {
const { items, totalQty, totalPrice, clear } = useCart()
if (totalQty() === 0) return null
return (
<div className="fixed bottom-4 right-4 bg-white shadow-lg p-4 rounded">
<p>共 {totalQty()} 件,总价 ¥{totalPrice()}</p>
<button onClick={clear}>清空</button>
</div>
)
}
7. 进阶:中间件 & DevTools
Zustand 的中间件就是洋葱模型,顺序从右到左执行。
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface BearState {
bears: number
increase: (by: number) => void
}
export const useBear = create<BearState>()(
devtools(
persist(
(set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}),
{ name: 'bear-storage' }
),
{ name: 'bear-store' } // Redux DevTools 中的实例名
)
)
打开浏览器 Redux DevTools,可以看到每一步 action 与状态变化。
8. 性能优化:selector 深度用法
// 只订阅 count,不订阅 inc/dec
const count = useCounter((s) => s.count)
// 等价于
const count = useCounter(({ count }) => count)
复杂计算用 shallow 或 memoized selector:
import { useShallow } from 'zustand/shallow'
const { a, b } = useStore(
useShallow((s) => ({ a: s.a, b: s.b }))
)
9. 与 React Context 对比
| 场景 | Context | Zustand |
|---|---|---|
| 仅父子通信 | ✅ | ✅ |
| 深层传递 | ❌ 需逐层 provider | ✅ 直接 import |
| 高频更新 | ❌ 触发大量 re-render | ✅ selector 精准订阅 |
| 包体积 | 0 | 2 kB |
10. 常见坑 & 排查清单
- 热更新丢状态
在 Vite 里给create包一层if (import.meta.hot) import.meta.hot.invalidate()。 - SSR 报错
localStorage is not defined
使用persist时加storage: typeof window !== 'undefined' ? window.localStorage : undefined。 - Zustand 4.4 之后必须写泛型
别忘了<State>泛型,否则类型推导失效。
11. 总结
- 30 行代码就能跑通全局状态。
- 异步、持久化、DevTools 全是官方中间件,一行搞定。
- 性能优化靠 selector,心智负担极低。
把 Zustand 当成“增强版 useState + 全局共享”,你就用对了。
12. 一键克隆完整示例
git clone https://github.com/yourname/zustand-demo.git
cd zustand-demo
npm i
npm run dev
Happy coding!
Comments NOTHING