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 })),
})),
)
这个中间件把 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 做调试。