• 简体中文
  • TypeScript 初学者指南

    Zustand 是一个轻量的状态管理器,尤其常与 React 搭配使用。Zustand 摒弃了 reducer、上下文和样板代码。 配合 TypeScript 使用时,你可以获得强类型的 store 状态、action 和选择器,并自带自动补全和编译期安全。

    在这篇基础指南中,我们将涵盖:

    • 创建带类型的 store(状态 + action)
    • 在 React 组件中以类型安全的方式使用 store
    • 以类型安全的方式重置 store
    • 提取并复用 Store 类型(用于 props、测试和工具函数)
    • 组合多个选择器并构建派生状态(带类型推断且不产生额外重新渲染)
    • 支持 TypeScript 的中间件(combine、devtools、persist)
    • 带类型化 API 响应的异步 action
    • 使用 createWithEqualityFn(增强版的 create store 函数)
    • 组织并协调多个 store

    创建带状态和 action 的 store

    这里我们用 TypeScript 接口来描述状态和 action。<BearState> 泛型会强制 store 符合这一结构。 这意味着如果你漏掉某个字段或用错类型,TypeScript 都会报错。与纯 JS 不同,这样能保证类型安全的状态管理。 create 函数使用柯里化形式,最终得到的 store 类型是 UseBoundStore<StoreApi<BearState>>。

    // store.ts
    import { create } from 'zustand'
    
    // Define types for state & actions
    interface BearState {
      bears: number
      food: string
      feed: (food: string) => void
    }
    
    // Create store using the curried form of `create`
    export const useBearStore = create<BearState>()((set) => ({
      bears: 2,
      food: 'honey',
      feed: (food) => set(() => ({ food })),
    }))

    在组件中使用 store

    在组件内部,你可以读取状态并调用 action。选择器 (s) => s.bears 只订阅你需要的部分。 这会减少重新渲染并提升性能。JS 也能做到这一点,但有了 TS,IDE 会自动补全状态字段。

    import { useBearStore } from './store'
    
    function BearCounter() {
      // Select only 'bears' to avoid unnecessary re-renders
      const bears = useBearStore((s) => s.bears)
      return <h1>{bears} bears around</h1>
    }

    重置 store

    在用户登出或“清除会话”之后,重置很有用。我们用 typeof initialState 来避免重复书写属性类型。 如果 initialState 发生变化,TypeScript 会自动更新。与 JS 相比,这更安全也更简洁。

    import { create } from 'zustand'
    
    const initialState = { bears: 0, food: 'honey' }
    
    // Reuse state type dynamically
    type BearState = typeof initialState & {
      increase: (by: number) => void
      reset: () => void
    }
    
    const useBearStore = create<BearState>()((set) => ({
      ...initialState,
      increase: (by) => set((s) => ({ bears: s.bears + by })),
      reset: () => set(initialState),
    }))
    
    function ResetZoo() {
      const { bears, increase, reset } = useBearStore()
    
      return (
        <div>
          <div>{bears}</div>
          <button onClick={() => increase(5)}>Increase by 5</button>
          <button onClick={reset}>Reset</button>
        </div>
      )
    }

    提取类型

    Zustand 提供了一个名为 ExtractState 的内置辅助类型。它适用于测试、工具函数或组件 props。 它返回 store 状态和 action 的完整类型,无需手动重新定义。提取 Store 类型的方式如下:

    // store.ts
    import { create, type ExtractState } from 'zustand'
    
    export const useBearStore = create((set) => ({
      bears: 3,
      food: 'honey',
      increase: (by: number) => set((s) => ({ bears: s.bears + by })),
    }))
    
    // Extract the type of the whole store state
    export type BearState = ExtractState<typeof useBearStore>

    在测试中使用提取出的类型:

    // test.cy.ts
    import { BearState } from './store.ts'
    
    test('should reset store', () => {
      const snapshot: BearState = useBearStore.getState()
      expect(snapshot.bears).toBeGreaterThanOrEqual(0)
    })

    以及在工具函数中:

    // util.ts
    import { BearState } from './store.ts'
    
    function logBearState(state: BearState) {
      console.log(`We have ${state.bears} bears eating ${state.food}`)
    }
    
    logBearState(useBearStore.getState())

    选择器

    多个选择器

    有时你需要不止一个属性。从选择器中返回一个对象,就能一次访问多个字段。 不过,直接从这个对象上解构属性可能会导致不必要的重新渲染。 为避免这种情况,建议用 useShallow 包裹选择器,当所选值在浅比较下仍然相等时,它可以阻止重新渲染。 这比订阅整个 store 更高效。TypeScript 能确保你不会意外拼错 bears 或 food。 关于 useShallow 的更多细节,请参阅 API 文档。

    import { create } from 'zustand'
    import { useShallow } from 'zustand/react/shallow'
    
    // Bear store with explicit types
    interface BearState {
      bears: number
      food: number
    }
    
    const useBearStore = create<BearState>()(() => ({
      bears: 2,
      food: 10,
    }))
    
    // In components, you can use both stores safely
    function MultipleSelectors() {
      const { bears, food } = useBearStore(
        useShallow((state) => ({ bears: state.bears, food: state.food })),
      )
    
      return (
        <div>
          We have {food} units of food for {bears} bears
        </div>
      )
    }

    用选择器派生状态

    并非所有值都需要直接存进 store —— 有些可以从现有状态计算出来。你可以用选择器派生值。 这样可以避免重复,让 store 保持精简。TypeScript 确保 bears 是数字,因此数学运算安全。

    import { create } from 'zustand'
    
    interface BearState {
      bears: number
      foodPerBear: number
    }
    
    const useBearStore = create<BearState>()(() => ({
      bears: 3,
      foodPerBear: 2,
    }))
    
    function TotalFood() {
      // Derived value: required amount food for all bears
      const totalFood = useBearStore((s) => s.bears * s.foodPerBear) // don't need to have extra property `{ totalFood: 6 }` in your Store
    
      return <div>We need ${totalFood} jars of honey</div>
    }

    中间件

    combine 中间件

    这个中间件把初始状态和 action 分开,让代码更清晰。 TS 会从状态和 action 自动推断类型,不需要写接口。 这与 JS 不同 —— 在 JS 中没有类型安全。这在 TypeScript 项目中是非常流行的写法。 更多细节请参阅 API 文档。

    import { create } from 'zustand'
    import { combine } from 'zustand/middleware'
    
    interface BearState {
      bears: number
      increase: () => void
    }
    
    // State + actions are separated
    export const useBearStore = create<BearState>()(
      combine({ bears: 0 }, (set) => ({
        increase: () => set((s) => ({ bears: s.bears + 1 })),
      })),
    )

    devtools 中间件

    这个中间件把 Zustand 连接到 Redux DevTools。你可以查看状态变化、进行时间旅行,并调试状态。 它在开发阶段极其有用。TS 确保即使在这种场景下,action 和状态依然接受类型检查。 更多细节请参阅 API 文档。

    import { create } from 'zustand'
    import { devtools } from 'zustand/middleware'
    
    interface BearState {
      bears: number
      increase: () => void
    }
    
    export const useBearStore = create<BearState>()(
      devtools((set) => ({
        bears: 0,
        increase: () => set((s) => ({ bears: s.bears + 1 })),
      })),
    )

    persist 中间件

    这个中间件会把你的 store 保存在 localStorage(或其他存储)中。这意味着你的小熊在页面刷新后依然存在。 对于重视持久化的应用来说这非常合适。在 TS 中,状态类型保持一致,因此不会有意外的运行时问题。 更多细节请参阅 API 文档。

    import { create } from 'zustand'
    import { persist } from 'zustand/middleware'
    
    interface BearState {
      bears: number
      increase: () => void
    }
    
    export const useBearStore = create<BearState>()(
      persist(
        (set) => ({
          bears: 0,
          increase: () => set((s) => ({ bears: s.bears + 1 })),
        }),
        { name: 'bear-storage' }, // localStorage key
      ),
    )

    异步 action

    action 可以是异步的,用于获取远程数据。这里我们获取小熊的数量并更新状态。 TS 会强制要求正确的 API 响应类型(BearData)。在 JS 中你可能会拼错 count —— 而 TS 能防止这种情况。

    import { create } from 'zustand'
    
    interface BearData {
      count: number
    }
    
    interface BearState {
      bears: number
      fetchBears: () => Promise<void>
    }
    
    export const useBearStore = create<BearState>()((set) => ({
      bears: 0,
      fetchBears: async () => {
        const res = await fetch('/api/bears')
        const data: BearData = await res.json()
    
        set({ bears: data.count })
      },
    }))

    createWithEqualityFn

    create 的变体,内置了相等性判断。如果你总是想使用自定义的相等性检查,它会很有用。 这种用法并不常见,但它展示了 Zustand 的灵活性。TS 依然保留完整的类型推断。 更多细节请参阅 API 文档。

    import { createWithEqualityFn } from 'zustand/traditional'
    import { shallow } from 'zustand/shallow'
    
    const useBearStore = createWithEqualityFn(() => ({
      bears: 0,
    }))
    
    const bears = useBearStore((s) => s.bears, Object.is)
    // or
    const bears = useBearStore((s) => ({ bears: s.bears }), shallow)

    多个 store

    你可以为不同的领域创建多个 store。例如,BearStore 管理小熊,FishStore 管理鱼。 这样能让状态彼此隔离,在较大的应用中更易于维护。使用 TypeScript 时,每个 store 都有自己的严格类型 —— 你不会意外把小熊和鱼混在一起。

    import { create } from 'zustand'
    
    // Bear store with explicit types
    interface BearState {
      bears: number
      addBear: () => void
    }
    
    const useBearStore = create<BearState>()((set) => ({
      bears: 2,
      addBear: () => set((s) => ({ bears: s.bears + 1 })),
    }))
    
    // Fish store with explicit types
    interface FishState {
      fish: number
      addFish: () => void
    }
    
    const useFishStore = create<FishState>()((set) => ({
      fish: 5,
      addFish: () => set((s) => ({ fish: s.fish + 1 })),
    }))
    
    // In components, you can use both stores safely
    function Zoo() {
      const { bears, addBear } = useBearStore()
      const { fish, addFish } = useFishStore()
    
      return (
        <div>
          <div>
            {bears} bears and {fish} fish
          </div>
          <button onClick={addBear}>Add bear</button>
          <button onClick={addFish}>Add fish</button>
        </div>
      )
    }

    结语

    Zustand 与 TypeScript 结合提供了良好的平衡:你既能保留小巧精简的 store 带来的简单性,又能获得强类型带来的安全性。 你不需要样板代码或复杂的模式 —— 状态和 action 并肩存在,类型完整,随时可用。 先用一个基础 store 掌握这一模式,然后逐步扩展:用 combine 获得更清晰的类型推断,用 persist 做持久化,用 devtools 做调试。