教程 zustand tutorial

中间件:Persist 持久化

Zustand 提供了一个强大的 persist 中间件,用于将 Store 的状态持久化到本地存储(如 localStorage、sessionStorage 或自定义存储)中。这意味着即使页面刷新或浏览器关闭,应用的状态也能被保存和恢复。

什么是 Persist 中间件?

persist 中间件允许你:

  • 状态持久化:将 Store 状态保存到浏览器的本地存储
  • 自动恢复:页面刷新后自动恢复之前的状态
  • 灵活配置:支持不同的存储引擎、键名自定义、序列化/反序列化等

安装与基本使用

安装依赖

persist 中间件是 Zustand 的核心功能之一,不需要额外安装。

基本用法

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

// 创建一个带有 persist 中间件的 Store
const useCounterStore = create(
  persist(
    (set, get) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
      reset: () => set({ count: 0 }),
    }),
    {
      name: 'counter-storage', // 存储在本地存储中的键名
    }
  )
)

在组件中使用

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>
  )
}

现在,当你刷新页面时,计数器的状态会自动从本地存储中恢复。

配置选项

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

javascript
const useStore = create(
  persist(
    (set, get) => ({
      // Store 定义
    }),
    {
      name: 'my-store', // 存储键名,必填
      storage: localStorage, // 存储引擎,默认为 localStorage
      version: 1, // 版本号,用于迁移
      migrate: (persistedState, version) => {
        // 状态迁移函数
        if (version === 0) {
          // 从版本 0 迁移到版本 1
          return { ...persistedState, newField: 'default' }
        }
        return persistedState
      },
      partialize: (state) => {
        // 部分持久化,只保存需要的数据
        return {
          count: state.count,
          user: state.user,
        }
      },
      serialize: (state) => JSON.stringify(state), // 序列化函数
      deserialize: (str) => JSON.parse(str), // 反序列化函数
    }
  )
)

主要配置选项说明

选项 类型 默认值 描述
name string - 存储在本地存储中的键名,必填项
storage Storage localStorage 存储引擎,可以是 localStorage、sessionStorage 或自定义存储
version number 0 版本号,用于状态迁移
migrate function (state, version) => state 状态迁移函数,当版本号变化时调用
partialize function (state) => state 部分持久化函数,用于选择需要保存的状态
serialize function JSON.stringify 序列化函数,将状态转换为字符串
deserialize function JSON.parse 反序列化函数,将字符串转换为状态

自定义存储引擎

你可以使用自定义的存储引擎,而不仅仅局限于浏览器的原生存储:

javascript
// 示例:使用 IndexedDB 作为存储引擎
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import idbKeyval from 'idb-keyval' // 需要安装 idb-keyval 库

// 创建自定义存储引擎
const indexedDBStorage = {
  getItem: async (name) => {
    const value = await idbKeyval.get(name)
    return value || null
  },
  setItem: async (name, value) => {
    await idbKeyval.set(name, value)
  },
  removeItem: async (name) => {
    await idbKeyval.del(name)
  },
}

const useStore = create(
  persist(
    (set, get) => ({
      // Store 定义
    }),
    {
      name: 'my-store',
      storage: indexedDBStorage, // 使用自定义的 IndexedDB 存储
    }
  )
)

部分持久化

使用 partialize 选项,你可以选择只持久化 Store 中的部分状态:

javascript
const useUserStore = create(
  persist(
    (set, get) => ({
      user: {
        id: 1,
        name: 'John Doe',
        email: '[email protected]',
      },
      isLoggedIn: true,
      theme: 'dark',
      // 临时状态,不需要持久化
      tempData: 'some temporary data',
      
      updateUser: (user) => set({ user }),
      toggleTheme: () => set((state) => ({ theme: state.theme === 'dark' ? 'light' : 'dark' })),
    }),
    {
      name: 'user-storage',
      partialize: (state) => ({
        // 只持久化这些字段
        user: state.user,
        isLoggedIn: state.isLoggedIn,
        theme: state.theme,
      }),
    }
  )
)

状态迁移

当你的应用版本升级,Store 结构发生变化时,可以使用 migrate 选项进行状态迁移:

javascript
const useTodoStore = create(
  persist(
    (set, get) => ({
      todos: [],
      addTodo: (text) => set((state) => ({
        todos: [...state.todos, { id: Date.now(), text, completed: false, createdAt: new Date() }],
      })),
      toggleTodo: (id) => set((state) => ({
        todos: state.todos.map((todo) => 
          todo.id === id ? { ...todo, completed: !todo.completed } : todo
        ),
      })),
    }),
    {
      name: 'todo-storage',
      version: 2, // 版本号从 1 升级到 2
      migrate: (persistedState, version) => {
        if (version === 1) {
          // 从版本 1 迁移到版本 2
          return {
            ...persistedState,
            // 为旧的 todos 添加 createdAt 字段
            todos: persistedState.todos.map((todo) => ({
              ...todo,
              createdAt: new Date().toISOString(),
            })),
          }
        }
        return persistedState
      },
    }
  )
)

与其他中间件结合使用

persist 中间件可以与其他 Zustand 中间件结合使用,例如 immer:

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

const useStore = create(
  persist(
    immer((set, get) => ({
      count: 0,
      user: { name: 'John', age: 30 },
      
      increment: () => set((state) => { state.count += 1 }),
      updateUser: (updates) => set((state) => { 
        Object.assign(state.user, updates) 
      }),
    })),
    {
      name: 'combined-storage',
    }
  )
)

手动控制持久化

你可以通过 Store 的 persist 属性手动控制持久化操作:

javascript
const useStore = create(
  persist(
    (set, get) => ({
      // Store 定义
    }),
    {
      name: 'my-store',
    }
  )
)

// 组件中使用
function MyComponent() {
  const store = useStore()
  
  // 手动触发持久化
  const forcePersist = () => {
    store.persist.rehydrate() // 强制重新恢复状态
  }
  
  // 清除持久化的状态
  const clearStorage = () => {
    store.persist.clearStorage() // 清除存储中的状态
  }
  
  // 获取持久化的状态
  const getPersistedState = async () => {
    const state = await store.persist.getStorageValue()
    console.log('Persisted state:', state)
  }
  
  return (
    <div>
      <button onClick={forcePersist}>重新恢复状态</button>
      <button onClick={clearStorage}>清除存储</button>
      <button onClick={getPersistedState}>获取持久化状态</button>
    </div>
  )
}

常见问题与注意事项

1. 循环引用问题

JSON.stringify 无法处理循环引用,如果你的状态中包含循环引用,需要提供自定义的 serialize 函数:

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

const useStore = create(
  persist(
    (set, get) => ({
      // 包含循环引用的状态
      node: { value: 1, parent: null },
      setParent: () => set((state) => {
        const newNode = { value: 2, parent: state.node }
        return { node: newNode }
      }),
    }),
    {
      name: 'circular-store',
      // 使用自定义序列化函数
      serialize: (state) => {
        // 处理循环引用
        const { node, ...rest } = state
        return JSON.stringify({
          ...rest,
          node: { value: node.value }, // 只序列化必要的字段
        })
      },
    }
  )
)

2. 性能考虑

  • 避免持久化大型对象:只持久化必要的数据
  • 合理使用 partialize:只保存需要的数据字段
  • 考虑使用 IndexedDB:对于大型数据集,考虑使用 IndexedDB 而不是 localStorage

3. 安全性

  • 敏感数据:不要将敏感数据(如密码、令牌)存储在客户端存储中
  • 数据验证:恢复状态时,始终验证数据的有效性

最佳实践总结

  1. 合理使用:只持久化必要的状态,避免过度使用
  2. 版本管理:使用版本号和迁移函数管理状态变化
  3. 数据验证:恢复状态时验证数据的有效性
  4. 性能优化:使用 partialize 减少持久化的数据量
  5. 安全考虑:避免存储敏感信息

在接下来的章节中,我们将学习 中间件:Immer 简化状态修改,了解如何使用 Immer 简化 Zustand 中的状态修改。