中间件: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. 安全性
- 敏感数据:不要将敏感数据(如密码、令牌)存储在客户端存储中
- 数据验证:恢复状态时,始终验证数据的有效性
最佳实践总结
- 合理使用:只持久化必要的状态,避免过度使用
- 版本管理:使用版本号和迁移函数管理状态变化
- 数据验证:恢复状态时验证数据的有效性
- 性能优化:使用
partialize减少持久化的数据量 - 安全考虑:避免存储敏感信息
在接下来的章节中,我们将学习 中间件:Immer 简化状态修改,了解如何使用 Immer 简化 Zustand 中的状态修改。