把 Zustand 讲明白:从 0 到 1 的实战指南

afcppe 发布于 23 天前 6 次阅读



1. 为什么选 Zustand?

维度Redux ToolkitZustand
包体积~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:一个函数,返回一个对象(状态 + 方法)。
  • hookcreate() 返回的自定义 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)

复杂计算用 shallowmemoized selector

import { useShallow } from 'zustand/shallow'

const { a, b } = useStore(
  useShallow((s) => ({ a: s.a, b: s.b }))
)

9. 与 React Context 对比

场景ContextZustand
仅父子通信
深层传递❌ 需逐层 provider✅ 直接 import
高频更新❌ 触发大量 re-render✅ selector 精准订阅
包体积02 kB

10. 常见坑 & 排查清单

  1. 热更新丢状态
    在 Vite 里给 create 包一层 if (import.meta.hot) import.meta.hot.invalidate()
  2. SSR 报错 localStorage is not defined
    使用 persist 时加 storage: typeof window !== 'undefined' ? window.localStorage : undefined
  3. 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!

此作者没有提供个人介绍。
最后更新于 2025-10-13