> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Zustand 完全指南
- URL: https://blog.vercanti.com/zustand-wan-quan-zhi-nan/
- Published: 2026-08-28T14:35:19.000Z
- Updated: 2026-08-28T14:58:32.000Z
- Description: Zustand 是 React 生态中一个轻量、无样板的状态管理库，基于 hooks，不依赖 Context Provider。 使用 create 函数定义 store，接收一个返回状态和操作的工厂函数： 通过选择器（selector）订阅 store 的特定字段，避免无关字段变化导致的重渲染： Zustand v4+ 推荐将泛型写在 create<T>() 上： 将 store 状态持久化到 localStorage 或其他存储： persist 配置项： 配合 immer 中间件，可以使用直接修改 state 的写法（内部仍为不可变更新）： 默认的
- Author: yellowdog
- Tags: 前端开发, React生态

> 官方文档：<https://docs.pmnd.rs/zustand/getting-started/introduction>  
> 适用版本：Zustand 5.x（2026-05-07 核实）

Zustand 是 React 生态中一个轻量、无样板的状态管理库，基于 hooks，不依赖 Context Provider。

---

## 安装

```bash
npm install zustand

```

---

## 基础用法

### 创建 Store

使用 `create` 函数定义 store，接收一个返回状态和操作的工厂函数：

```typescript
import { create } from 'zustand'

interface CounterStore {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

const useCounterStore = create<CounterStore>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 })
}))

```

### set 函数的两种用法

| 用法                             | 说明                                |
| ------------------------------ | --------------------------------- |
| set({ key: value })            | 对象形式，与现有 state **浅合并**            |
| set(state => ({ key: value })) | 函数形式，接收当前 state，返回要合并的部分          |
| set({ ... }, true)             | 第二个参数为 true 时，**完全替换** state（不合并） |

```typescript
// 浅合并：只更新 count，其他字段不变
set({ count: 5 })

// 函数形式：基于当前值计算
set((state) => ({ count: state.count + 1 }))

// 完全替换（慎用，会删除所有其他字段）
set({ count: 0 }, true)

```

### 在 action 中使用 get 读取当前 state

```typescript
import { create } from 'zustand'

interface CartStore {
  items: string[]
  addItem: (item: string) => void
  removeItem: (item: string) => void
  total: () => number
}

const useCartStore = create<CartStore>((set, get) => ({
  items: [],

  addItem: (item) => {
    // 通过 get() 获取最新 state，适合在需要读取后再写入的场景
    const current = get().items
    if (!current.includes(item)) {
      set({ items: [...current, item] })
    }
  },

  removeItem: (item) => {
    set((state) => ({
      items: state.items.filter((i) => i !== item)
    }))
  },

  // 派生计算也可以作为函数放在 store 内
  total: () => get().items.length
}))

```

### 在 React 组件中使用

通过选择器（selector）订阅 store 的特定字段，避免无关字段变化导致的重渲染：

```typescript
import React from 'react'

function Counter() {
  // 只订阅 count 字段，其他字段变化不会触发重渲染
  const count = useCounterStore((state) => state.count)
  const increment = useCounterStore((state) => state.increment)
  const reset = useCounterStore((state) => state.reset)

  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+1</button>
      <button onClick={reset}>重置</button>
    </div>
  )
}

```

---

## TypeScript 支持

### 类型定义方式

Zustand v4+ 推荐将泛型写在 `create<T>()` 上：

```typescript
import { create } from 'zustand'

// 分离接口定义，便于复用和测试
interface UserState {
  user: User | null
  isLoading: boolean
}

interface UserActions {
  fetchUser: (id: number) => Promise<void>
  clearUser: () => void
}

type UserStore = UserState & UserActions

const useUserStore = create<UserStore>((set) => ({
  user: null,
  isLoading: false,

  fetchUser: async (id) => {
    set({ isLoading: true })
    try {
      const user = await api.getUser(id)
      set({ user, isLoading: false })
    } catch {
      set({ isLoading: false })
    }
  },

  clearUser: () => set({ user: null })
}))

```

---

## 中间件

### persist：持久化

将 store 状态持久化到 `localStorage` 或其他存储：

```typescript
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

const useSettingsStore = create<SettingsStore>()(
  persist(
    (set) => ({
      theme: 'light',
      language: 'zh-CN',
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language })
    }),
    {
      name: 'settings-storage',
      storage: createJSONStorage(() => localStorage)
    }
  )
)

```

`persist` 配置项：

| 参数            | 类型                                      | 默认值          | 说明                            |
| ------------- | --------------------------------------- | ------------ | ----------------------------- |
| name          | string                                  | 必填           | localStorage 的 key 名称         |
| storage       | PersistStorage                          | localStorage | 存储实现，可替换为 sessionStorage 或自定义 |
| partialize    | (state) => Partial<State>               | 保存全部         | 只持久化部分字段                      |
| version       | number                                  | 0            | 数据版本号，版本不匹配时触发 migrate        |
| migrate       | (persistedState, version) => State      | undefined    | 版本迁移函数                        |
| merge         | (persistedState, currentState) => State | 浅合并          | 自定义合并逻辑                       |
| skipHydration | boolean                                 | false        | 跳过初始水合（SSR 场景）                |

```typescript
persist(
  (set) => ({ ... }),
  {
    name: 'auth-storage',
    // 只持久化 token，不持久化临时状态
    partialize: (state) => ({ token: state.token }),
    // 版本迁移示例
    version: 1,
    migrate: (persistedState: any, version) => {
      if (version === 0) {
        // 从旧格式迁移
        return { token: persistedState.authToken }
      }
      return persistedState
    }
  }
)

```

### immer：支持 mutation 写法

配合 `immer` 中间件，可以使用直接修改 state 的写法（内部仍为不可变更新）：

```typescript
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

interface TreeStore {
  nodes: Record<string, { name: string; children: string[] }>
  addChild: (parentId: string, childId: string) => void
}

const useTreeStore = create<TreeStore>()(
  immer((set) => ({
    nodes: {},
    addChild: (parentId, childId) => {
      set((state) => {
        // 可以直接 mutation，immer 会转换为不可变操作
        state.nodes[parentId]?.children.push(childId)
      })
    }
  }))
)

```

### devtools：Redux DevTools 集成

```typescript
import { devtools } from 'zustand/middleware'

const useStore = create<Store>()(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment')
      //                                                                        ^ action 名称，在 DevTools 中显示
    }),
    { name: 'CounterStore' }  // DevTools 中显示的 store 名称
  )
)

```

### subscribeWithSelector：细粒度订阅

默认的 `subscribe` 在任何字段变化时都会触发，`subscribeWithSelector` 允许只订阅特定字段：

```typescript
import { subscribeWithSelector } from 'zustand/middleware'

const useStore = create<Store>()(
  subscribeWithSelector((set) => ({
    count: 0,
    name: 'default',
    increment: () => set((state) => ({ count: state.count + 1 }))
  }))
)

// 只监听 count 变化，name 变化不会触发
const unsubscribe = useStore.subscribe(
  (state) => state.count,
  (count, prevCount) => {
    console.log(`count changed: ${prevCount} -> ${count}`)
  }
)

// 取消订阅
unsubscribe()

```

### 组合多个中间件

```typescript
const useStore = create<Store>()(
  devtools(
    persist(
      immer((set) => ({
        // ...
      })),
      { name: 'my-store' }
    ),
    { name: 'MyStore' }
  )
)

```

---

## 高级模式

### 切片（Slice）模式

当 store 较大时，按功能拆分为多个 slice 再组合：

```typescript
import { create, StateCreator } from 'zustand'

// 定义 bear slice
interface BearSlice {
  bears: number
  addBear: () => void
}

const createBearSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  BearSlice
> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 }))
})

// 定义 fish slice
interface FishSlice {
  fishes: number
  addFish: () => void
}

const createFishSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  FishSlice
> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 }))
})

// 组合 store
const useBoundStore = create<BearSlice & FishSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a)
}))

```

### 异步 action

Zustand 的 action 本身就是普通函数，直接使用 `async/await` 即可：

```typescript
interface PostStore {
  posts: Post[]
  isLoading: boolean
  error: string | null
  fetchPosts: () => Promise<void>
}

const usePostStore = create<PostStore>((set) => ({
  posts: [],
  isLoading: false,
  error: null,

  fetchPosts: async () => {
    set({ isLoading: true, error: null })
    try {
      const response = await fetch('/api/posts')
      const posts = await response.json()
      set({ posts, isLoading: false })
    } catch (err) {
      set({ error: (err as Error).message, isLoading: false })
    }
  }
}))

```

### 在组件外访问 store

Zustand store 本身是一个独立的对象，不依赖 React 上下文，可以在任何地方使用：

```typescript
// 读取当前 state（非响应式，适合一次性读取）
const currentCount = useCounterStore.getState().count

// 更新 state
useCounterStore.setState({ count: 10 })
useCounterStore.setState((state) => ({ count: state.count + 1 }))

// 订阅变化（适合在非 React 环境中使用，如工具函数、class 等）
const unsubscribe = useCounterStore.subscribe((state) => {
  console.log('count changed:', state.count)
})

```

### 订阅 store 变化

```typescript
// 基础订阅（任何字段变化都触发）
const unsubscribe = useStore.subscribe((newState, prevState) => {
  if (newState.count !== prevState.count) {
    sendAnalytics('count_changed', { value: newState.count })
  }
})

// 在 React 组件外使用，需要在适当时机取消订阅
// 例如在模块销毁时：
window.addEventListener('beforeunload', unsubscribe)

```

---

## 与其他方案对比

### vs Redux Toolkit

| 对比项         | Zustand    | Redux Toolkit                |
| ----------- | ---------- | ---------------------------- |
| 样板代码        | 极少         | 中等（slice/action/selector 分层） |
| 学习曲线        | 低          | 中等                           |
| DevTools 支持 | 通过中间件支持    | 原生支持                         |
| 适合场景        | 中小型应用、快速开发 | 大型团队、复杂业务逻辑                  |
| 时间旅行调试      | 有限支持       | 完整支持                         |
| 代码组织约束      | 无约束（灵活）    | 有约束（规范）                      |

### vs Jotai

| 对比项      | Zustand             | Jotai          |
| -------- | ------------------- | -------------- |
| 思维模型     | 集中式 store（类似 Redux） | 原子化（类似 Recoil） |
| state 组织 | 按模块聚合               | 按原子分散          |
| 适合场景     | 有明确模块边界的业务状态        | 大量细粒度、相互依赖的状态  |
| 代码量      | 需要定义 store 结构       | 原子定义简洁         |

### vs TanStack Query 的分工

两者不是竞争关系，而是互补：

| 工具             | 管理的状态类型         | 典型场景                 |
| -------------- | --------------- | -------------------- |
| TanStack Query | 服务端状态（异步数据）     | API 数据获取、缓存、同步       |
| Zustand        | 客户端状态（本地 UI 状态） | 用户偏好、UI 交互状态、跨组件共享数据 |

推荐：服务端数据用 TanStack Query，本地 UI 状态用 Zustand，二者搭配使用。

---

## 最佳实践

### 使用细粒度选择器

```typescript
// 不推荐：订阅整个 store，任何字段变化都触发重渲染
const store = useUserStore()

// 推荐：只订阅需要的字段
const username = useUserStore((state) => state.user?.name)
const isLoading = useUserStore((state) => state.isLoading)

```

### 选择器返回对象时使用 shallow 比较

```typescript
import { shallow } from 'zustand/shallow'

// 返回对象时，默认用 Object.is 比较会导致每次都重渲染
// 使用 shallow 进行浅比较
const { count, name } = useStore(
  (state) => ({ count: state.count, name: state.name }),
  shallow
)

```

---

## 踩坑与注意事项

### 直接解构 store 会失去响应性

```typescript
// 错误：解构赋值后，count 是普通变量，不具有响应性
const { count } = useCounterStore()
// count 不会随 store 变化自动更新

// 正确：通过选择器订阅
const count = useCounterStore((state) => state.count)

```

### persist 中间件对 Map/Set 的序列化限制

`JSON.stringify` 无法正确序列化 `Map`、`Set`、`Date` 等类型，`persist` 默认使用 JSON 序列化，这些类型会丢失：

```typescript
// 问题复现：Map 经过 JSON 序列化后变为 {}
const map = new Map('key', 'value')
JSON.parse(JSON.stringify(map))  // 输出 {}

// 解决方案：自定义 storage，手动处理序列化
const useStore = create<Store>()(
  persist(
    (set) => ({
      dataMap: new Map<string, string>()
    }),
    {
      name: 'store-with-map',
      storage: createJSONStorage(() => localStorage, {
        replacer: (key, value) => {
          if (value instanceof Map) {
            return { __type: 'Map', entries: Array.from(value.entries()) }
          }
          return value
        },
        reviver: (key, value) => {
          if (value?.__type === 'Map') {
            return new Map(value.entries)
          }
          return value
        }
      })
    }
  )
)

```

### 服务端渲染（SSR）场景的 hydration 问题

在 Next.js 等 SSR 框架中，服务端和客户端的初始 state 可能不一致，导致 hydration 报错。使用 `skipHydration` 并手动调用 `rehydrate`：

```typescript
const useStore = create<Store>()(
  persist(
    (set) => ({ ... }),
    {
      name: 'ssr-store',
      skipHydration: true  // 跳过自动 hydration
    }
  )
)

// 在客户端组件中手动触发 hydration
useEffect(() => {
  useStore.persist.rehydrate()
}, [])

```

### 避免在 store 外部直接修改 state 对象

```typescript
// 错误：直接修改 getState() 返回的对象不会触发响应
const state = useStore.getState()
state.count = 10  // 不会触发任何订阅

// 正确：通过 setState 触发响应
useStore.setState({ count: 10 })

```

---

## 常见陷阱

### 陷阱：在 `set` 函数外直接修改 state 对象

**现象：** `state.items.push(item)` 后组件不重新渲染，数据已变但 UI 未更新。  
**原因：** Zustand 用浅比较检测 state 变化，直接 mutate 不改变引用，订阅者不会被通知。  
**解决：** 始终通过 `set` 返回新对象（配合 Immer 插件可以写可变风格）：

```ts
// 错误：直接 push 不触发更新
set(state => { state.items.push(item) })

// 正确：返回新数组
set(state => ({ items: [...state.items, item] }))

// 或用 Immer 中间件
import { immer } from 'zustand/middleware/immer'
const useStore = create(immer((set) => ({
  items: [],
  addItem: (item) => set(state => { state.items.push(item) }),
})));

```

### 陷阱：在 Store 中订阅整个 state 导致不必要重渲染

**现象：** `const store = useStore()` 后，任何 state 字段变化都导致组件重渲染，包括无关字段。  
**原因：** 不带 selector 的 `useStore()` 订阅整个 state 对象，任何变化都会触发重渲染。  
**解决：** 用 selector 精确订阅需要的字段：

```ts
// 差：订阅整个 state
const { count, user } = useStore();

// 好：各自订阅各自的字段
const count = useStore(state => state.count);
const user = useStore(state => state.user);

```

### 陷阱：`persist` 中间件存储旧 schema 导致 hydration 错误

**现象：** 升级 store 结构后，localStorage 中仍是旧 schema 数据，导致类型错误或意外行为。  
**原因：** `persist` 从存储层恢复数据时不做 schema 校验，旧结构直接合并到新 state。  
**解决：** 配置 `version` 和 `migrate` 函数处理 schema 升级：

```ts
persist(store, {
  name: 'app-store',
  version: 2,
  migrate: (persistedState: any, version) => {
    if (version === 1) return { ...persistedState, newField: 'default' };
    return persistedState;
  },
})

```

---

## 参见

[React完全指南](https://blog.vercanti.com/react-wan-quan-zhi-nan/)  
[TanStack Query 完全指南](https://blog.vercanti.com/tanstack-query-wan-quan-zhi-nan/)