教程 zustand tutorial

调试神器:Redux DevTools

Zustand 提供了与 Redux DevTools 浏览器扩展的无缝集成,使你能够可视化地调试和监控 Store 的状态变化。Redux DevTools 是一个强大的调试工具,它可以帮助你:

  • 查看状态历史:追踪状态的每一次变化
  • 时间旅行调试:回溯到之前的状态,重现 bug
  • 检查 action:查看触发状态变化的 action 及其参数
  • 比较状态差异:查看两次状态变化之间的具体差异
  • 导出/导入状态:保存和恢复应用状态

安装与基本使用

安装 Redux DevTools 浏览器扩展

首先,你需要在浏览器中安装 Redux DevTools 扩展:

在 Zustand 中集成 DevTools

Zustand 提供了一个 devtools 中间件,用于与 Redux DevTools 集成:

javascript
import { create } from 'zustand'
import { devtools } from 'zustand/middleware' // 导入 devtools 中间件

// 创建一个带有 devtools 中间件的 Store
const useCounterStore = create(
  devtools(
    (set, get) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
      reset: () => set({ count: 0 }),
    }),
    {
      name: 'counter-store', // DevTools 中的实例名称
    }
  )
)

在组件中使用

jsx
function Counter() {
  const { count, increment, decrement, reset } = useCounterStore()

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

使用 Redux DevTools 调试

安装并配置好 DevTools 后,你可以通过以下步骤使用它来调试 Zustand Store:

  1. 打开 DevTools:在浏览器中按 F12 打开开发者工具,切换到 “Redux” 或 “Redux DevTools” 标签页
  2. 查看状态历史:每次触发 action 时,DevTools 会记录一条新的状态变更记录
  3. 检查状态:在 “State” 面板中查看当前的完整状态
  4. 检查 action:在 “Action” 面板中查看触发状态变化的 action 及其参数
  5. 时间旅行调试:点击历史记录中的任意一条记录,应用会回溯到该状态
  6. 比较差异:选择两条历史记录,点击 “Diff” 按钮查看状态差异

配置选项

devtools 中间件提供了丰富的配置选项:

javascript
const useStore = create(
  devtools(
    (set, get) => ({
      // Store 定义
    }),
    {
      name: 'my-store', // DevTools 中的实例名称
      enabled: true, // 是否启用 DevTools
      anonymousActionType: 'anonymous-action', // 匿名 action 的类型名称
      serialize: { // 序列化配置
        options: {
          // JSON.stringify 的配置选项
          undefined: true,
          function: true,
          symbol: true,
          map: true,
          set: true,
        },
      },
    }
  )
)

主要配置选项说明

选项 类型 默认值 描述
name string - DevTools 中的实例名称,用于区分多个 Store
enabled boolean true 是否启用 DevTools
anonymousActionType string ‘anonymous-action’ 匿名 action 的类型名称
serialize object - 序列化配置,用于控制状态的序列化方式

命名 Action

默认情况下,Zustand 会为 action 生成匿名名称。你可以通过在 set 函数中提供 action 名称来为 action 命名:

javascript
const useTodoStore = create(
  devtools(
    (set, get) => ({
      todos: [],
      
      // 命名 action:'addTodo'
      addTodo: (text) => set(
        (state) => ({ todos: [...state.todos, { id: Date.now(), text, completed: false }] }),
        false, // 是否应该替换整个状态
        'addTodo' // action 名称
      ),
      
      // 命名 action:'toggleTodo'
      toggleTodo: (id) => set(
        (state) => ({
          todos: state.todos.map((todo) =>
            todo.id === id ? { ...todo, completed: !todo.completed } : todo
          )
        }),
        false,
        'toggleTodo'
      ),
    }),
    { name: 'todo-store' }
  )
)

为 action 命名可以使 DevTools 中的调试信息更加清晰和易于理解。

与其他中间件结合使用

devtools 中间件可以与其他 Zustand 中间件(如 persist 和 immer)结合使用:

javascript
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

const useTodoStore = create(
  persist(
    devtools(
      immer((set, get) => ({
        todos: [],
        addTodo: (text) => set((state) => {
          state.todos.push({ id: Date.now(), text, completed: false })
        }, false, 'addTodo'), // 命名 action
        toggleTodo: (id) => set((state) => {
          const todo = state.todos.find((todo) => todo.id === id)
          if (todo) {
            todo.completed = !todo.completed
          }
        }, false, 'toggleTodo'), // 命名 action
      })),
      { name: 'todo-store' }
    ),
    { name: 'todo-storage' }
  )
)

注意:中间件的顺序很重要,一般建议将 devtools 放在其他中间件的内层,这样可以捕获到所有中间件处理后的 action 和状态变化。

高级用法

时间旅行调试

时间旅行调试是 Redux DevTools 最强大的功能之一,它允许你回溯到之前的状态:

  1. 在 DevTools 中选择一条历史记录
  2. 应用会自动回溯到该状态
  3. 你可以在该状态下继续调试,重现 bug

状态差异比较

DevTools 允许你比较两次状态变化之间的具体差异:

  1. 在 DevTools 中按住 Ctrl 键(或 Cmd 键)选择两条历史记录
  2. 点击 “Diff” 按钮
  3. DevTools 会显示这两次状态之间的具体差异

导出/导入状态

你可以导出当前状态,稍后再导入:

  1. 在 DevTools 中点击 “Export” 按钮导出状态
  2. 稍后点击 “Import” 按钮导入之前导出的状态
  3. 应用会恢复到导入的状态

禁用特定 Action

你可以通过配置禁用特定的 action:

javascript
const useStore = create(
  devtools(
    (set, get) => ({
      // Store 定义
      sensitiveAction: () => set((state) => {
        // 敏感操作,不希望在 DevTools 中显示
        return { ...state, sensitiveData: '...' }
      }),
    }),
    {
      name: 'my-store',
      // 禁用特定的 action
      enabled: (state, action) => {
        return action.type !== 'sensitiveAction'
      },
    }
  )
)

性能考虑

虽然 Redux DevTools 提供了强大的调试功能,但在生产环境中,你可能希望禁用它以提高性能:

javascript
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

const useStore = create(
  devtools(
    (set, get) => ({
      // Store 定义
    }),
    {
      name: 'my-store',
      enabled: process.env.NODE_ENV !== 'production', // 只在开发环境启用
    }
  )
)

最佳实践总结

  1. 为 Action 命名:为 action 提供清晰的名称,使调试信息更加易于理解
  2. 只在开发环境启用:在生产环境中禁用 DevTools 以提高性能
  3. 与其他中间件结合使用:根据需求,将 DevTools 与 persist、immer 等中间件结合使用
  4. 保护敏感数据:避免在 DevTools 中显示敏感数据
  5. 合理使用时间旅行:利用时间旅行功能重现和调试 bug
  6. 导出/导入状态:使用导出/导入功能保存和恢复应用状态

常见问题与解决方案

1. DevTools 中看不到状态变化

问题:在组件中触发了 action,但在 DevTools 中看不到状态变化

解决方案:

  • 确保已安装 Redux DevTools 浏览器扩展
  • 确保已正确导入并使用 devtools 中间件
  • 确保 enabled 配置选项为 true
  • 检查中间件的顺序是否正确

2. Action 名称显示为 “anonymous-action”

问题:在 DevTools 中,所有 action 的名称都显示为 “anonymous-action”

解决方案:为 action 提供名称:

javascript
addTodo: (text) => set(
  (state) => ({ todos: [...state.todos, { id: Date.now(), text, completed: false }] }),
  false,
  'addTodo' // 为 action 命名
)

3. 状态显示为 “[object Object]”

问题:在 DevTools 中,状态显示为 “[object Object]” 而不是具体的状态内容

解决方案:检查序列化配置,确保状态可以被正确序列化:

javascript
devtools(
  (set, get) => ({
    // Store 定义
  }),
  {
    name: 'my-store',
    serialize: {
      options: {
        undefined: true,
        function: true,
        symbol: true,
      },
    },
  }
)

4. 生产环境性能问题

问题:在生产环境中,应用性能下降

解决方案:在生产环境中禁用 DevTools:

javascript
devtools(
  (set, get) => ({
    // Store 定义
  }),
  {
    name: 'my-store',
    enabled: process.env.NODE_ENV !== 'production',
  }
)

在接下来的章节中,我们将学习 TypeScript 类型定义指南,了解如何在 Zustand 中使用 TypeScript 进行类型定义。