状态持久化 #

什么是状态持久化? #

状态持久化是指将应用状态保存到本地存储(如 localStorage、sessionStorage),在页面刷新或重新打开应用时能够恢复状态。

为什么需要持久化? #

text
持久化应用场景
├── 用户偏好 ──── 主题、语言、布局设置
├── 表单数据 ──── 防止意外丢失
├── 登录状态 ──── 免重复登录
├── 购物车 ────── 保留购物信息
└── 缓存数据 ──── 减少网络请求

基本使用 #

最简单的持久化 #

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

const useStore = create(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'counter-storage',
    }
  )
)

完整类型定义 #

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

interface State {
  count: number
  name: string
  increment: () => void
  setName: (name: string) => void
}

const useStore = create<State>()(
  persist(
    (set) => ({
      count: 0,
      name: '',
      increment: () => set((state) => ({ count: state.count + 1 })),
      setName: (name) => set({ name }),
    }),
    {
      name: 'my-storage',
      storage: createJSONStorage(() => localStorage),
    }
  )
)

存储引擎 #

localStorage #

数据永久保存,除非手动清除:

tsx
import { persist, createJSONStorage } from 'zustand/middleware'

persist(
  (set) => ({ /* store */ }),
  {
    name: 'my-storage',
    storage: createJSONStorage(() => localStorage),
  }
)

sessionStorage #

数据在标签页关闭后清除:

tsx
persist(
  (set) => ({ /* store */ }),
  {
    name: 'my-storage',
    storage: createJSONStorage(() => sessionStorage),
  }
)

自定义存储引擎 #

IndexedDB 存储 #

tsx
const indexedDBStorage = {
  getItem: async (name: string): Promise<string | null> => {
    return new Promise((resolve) => {
      const request = indexedDB.open('zustand-store', 1)
      
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result
        if (!db.objectStoreNames.contains('stores')) {
          db.createObjectStore('stores')
        }
      }
      
      request.onsuccess = (event) => {
        const db = (event.target as IDBOpenDBRequest).result
        const transaction = db.transaction('stores', 'readonly')
        const store = transaction.objectStore('stores')
        const getRequest = store.get(name)
        
        getRequest.onsuccess = () => {
          resolve(getRequest.result || null)
        }
        
        getRequest.onerror = () => {
          resolve(null)
        }
      }
      
      request.onerror = () => {
        resolve(null)
      }
    })
  },
  
  setItem: async (name: string, value: string): Promise<void> => {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('zustand-store', 1)
      
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result
        if (!db.objectStoreNames.contains('stores')) {
          db.createObjectStore('stores')
        }
      }
      
      request.onsuccess = (event) => {
        const db = (event.target as IDBOpenDBRequest).result
        const transaction = db.transaction('stores', 'readwrite')
        const store = transaction.objectStore('stores')
        store.put(value, name)
        
        transaction.oncomplete = () => resolve()
        transaction.onerror = () => reject(transaction.error)
      }
      
      request.onerror = () => {
        reject(request.error)
      }
    })
  },
  
  removeItem: async (name: string): Promise<void> => {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('zustand-store', 1)
      
      request.onsuccess = (event) => {
        const db = (event.target as IDBOpenDBRequest).result
        const transaction = db.transaction('stores', 'readwrite')
        const store = transaction.objectStore('stores')
        store.delete(name)
        
        transaction.oncomplete = () => resolve()
        transaction.onerror = () => reject(transaction.error)
      }
    })
  },
}

// 使用
persist(
  (set) => ({ /* store */ }),
  {
    name: 'my-storage',
    storage: indexedDBStorage,
  }
)

加密存储 #

tsx
import CryptoJS from 'crypto-js'

const SECRET_KEY = 'your-secret-key'

const encryptedStorage = {
  getItem: (name: string): string | null => {
    const encrypted = localStorage.getItem(name)
    if (!encrypted) return null
    
    try {
      const decrypted = CryptoJS.AES.decrypt(encrypted, SECRET_KEY)
      return decrypted.toString(CryptoJS.enc.Utf8)
    } catch {
      return null
    }
  },
  
  setItem: (name: string, value: string): void => {
    const encrypted = CryptoJS.AES.encrypt(value, SECRET_KEY).toString()
    localStorage.setItem(name, encrypted)
  },
  
  removeItem: (name: string): void => {
    localStorage.removeItem(name)
  },
}

// 使用
persist(
  (set) => ({ /* store */ }),
  {
    name: 'secure-storage',
    storage: encryptedStorage,
  }
)

部分持久化 #

partialize 选项 #

只持久化需要的状态:

tsx
interface State {
  // 需要持久化
  user: User | null
  theme: 'light' | 'dark'
  
  // 不需要持久化
  isLoading: boolean
  error: string | null
  
  // Actions
  setUser: (user: User) => void
  setTheme: (theme: 'light' | 'dark') => void
}

const useStore = create<State>()(
  persist(
    (set) => ({
      user: null,
      theme: 'light',
      isLoading: false,
      error: null,
      
      setUser: (user) => set({ user }),
      setTheme: (theme) => set({ theme }),
    }),
    {
      name: 'user-preferences',
      partialize: (state) => ({
        user: state.user,
        theme: state.theme,
      }),
    }
  )
)

使用函数过滤 #

tsx
persist(
  (set) => ({
    count: 0,
    name: '',
    temporaryData: null,
    
    increment: () => set((state) => ({ count: state.count + 1 })),
  }),
  {
    name: 'my-storage',
    partialize: (state) => {
      const { temporaryData, ...persisted } = state
      return persisted
    },
  }
)

使用数组指定字段 #

tsx
persist(
  (set) => ({
    count: 0,
    name: '',
    temp: '',
  }),
  {
    name: 'my-storage',
    partialize: ['count', 'name'] as const,
  }
)

数据迁移 #

版本控制 #

tsx
const useStore = create(
  persist(
    (set) => ({
      count: 0,
      version: 2, // 当前版本
    }),
    {
      name: 'counter-storage',
      version: 2, // 存储版本
    }
  )
)

migrate 函数 #

处理版本升级时的数据迁移:

tsx
interface State {
  user: {
    name: string
    email: string
    avatar?: string  // v2 新增
  }
  preferences: {
    theme: 'light' | 'dark'
    language: string
  }
}

const useStore = create<State>()(
  persist(
    (set) => ({
      user: {
        name: '',
        email: '',
        avatar: '',
      },
      preferences: {
        theme: 'light',
        language: 'zh-CN',
      },
    }),
    {
      name: 'user-storage',
      version: 2,
      migrate: (persisted: any, version: number) => {
        // v0 -> v1: 添加 preferences
        if (version < 1) {
          persisted.preferences = {
            theme: 'light',
            language: 'zh-CN',
          }
        }
        
        // v1 -> v2: 添加 avatar 字段
        if (version < 2) {
          persisted.user.avatar = ''
        }
        
        return persisted
      },
    }
  )
)

复杂迁移示例 #

tsx
interface UserV1 {
  username: string
  email: string
}

interface UserV2 {
  profile: {
    name: string
    email: string
    avatar: string
  }
  settings: {
    theme: 'light' | 'dark'
    notifications: boolean
  }
}

const useUserStore = create<UserV2>()(
  persist(
    (set) => ({
      profile: {
        name: '',
        email: '',
        avatar: '',
      },
      settings: {
        theme: 'light',
        notifications: true,
      },
    }),
    {
      name: 'user-storage',
      version: 2,
      migrate: (persisted: any, version: number) => {
        if (version === 0) {
          // v0: 扁平结构
          return {
            profile: {
              name: persisted.username || '',
              email: persisted.email || '',
              avatar: '',
            },
            settings: {
              theme: 'light',
              notifications: true,
            },
          }
        }
        
        if (version === 1) {
          // v1: 添加 settings
          return {
            ...persisted,
            settings: {
              theme: 'light',
              notifications: true,
            },
          }
        }
        
        return persisted
      },
    }
  )
)

恢复状态 #

onRehydrateStorage #

在状态恢复后执行回调:

tsx
persist(
  (set) => ({
    user: null,
    token: null,
    isAuthenticated: false,
    
    login: (user, token) => set({ user, token, isAuthenticated: true }),
    logout: () => set({ user: null, token: null, isAuthenticated: false }),
  }),
  {
    name: 'auth-storage',
    onRehydrateStorage: () => (state) => {
      console.log('状态已恢复:', state)
      
      // 恢复后验证 token
      if (state?.token) {
        validateToken(state.token).then((valid) => {
          if (!valid) {
            state.logout()
          }
        })
      }
    },
  }
)

手动触发恢复 #

tsx
const useStore = create(
  persist(
    (set) => ({
      count: 0,
    }),
    {
      name: 'counter-storage',
    }
  )
)

// 手动恢复
const rehydrate = async () => {
  await useStore.persist.rehydrate()
}

清除持久化数据 #

tsx
// 清除特定 store 的持久化数据
useStore.persist.clearStorage()

// 或使用 API
const api = useStore.getState()
if ('persist' in api) {
  (api as any).persist.clearStorage()
}

高级用法 #

条件持久化 #

tsx
const useStore = create(
  persist(
    (set) => ({
      count: 0,
      shouldPersist: true,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'conditional-storage',
      // 根据条件决定是否持久化
      partialize: (state) => 
        state.shouldPersist ? { count: state.count } : {},
    }
  )
)

多存储实例 #

tsx
const createUserStore = (userId: string) => 
  create(
    persist(
      (set) => ({
        data: null,
        setData: (data) => set({ data }),
      }),
      {
        name: `user-${userId}-storage`,
      }
    )
  )

// 使用
const useUser1Store = createUserStore('user-1')
const useUser2Store = createUserStore('user-2')

SSR 兼容 #

tsx
const useStore = create(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'ssr-storage',
      storage: createJSONStorage(() => {
        // SSR 时返回空存储
        if (typeof window === 'undefined') {
          return {
            getItem: () => null,
            setItem: () => {},
            removeItem: () => {},
          }
        }
        return localStorage
      }),
    }
  )
)

实际案例 #

用户偏好设置 #

tsx
interface Preferences {
  theme: 'light' | 'dark' | 'system'
  language: string
  fontSize: 'small' | 'medium' | 'large'
  sidebarCollapsed: boolean
  notifications: {
    email: boolean
    push: boolean
    sound: boolean
  }
}

interface PreferencesState extends Preferences {
  setTheme: (theme: Preferences['theme']) => void
  setLanguage: (language: string) => void
  setFontSize: (size: Preferences['fontSize']) => void
  toggleSidebar: () => void
  updateNotifications: (notifications: Partial<Preferences['notifications']>) => void
  reset: () => void
}

const defaultPreferences: Preferences = {
  theme: 'system',
  language: 'zh-CN',
  fontSize: 'medium',
  sidebarCollapsed: false,
  notifications: {
    email: true,
    push: true,
    sound: false,
  },
}

const usePreferencesStore = create<PreferencesState>()(
  persist(
    (set) => ({
      ...defaultPreferences,
      
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
      setFontSize: (fontSize) => set({ fontSize }),
      toggleSidebar: () => set((state) => ({ 
        sidebarCollapsed: !state.sidebarCollapsed 
      })),
      updateNotifications: (notifications) => set((state) => ({
        notifications: { ...state.notifications, ...notifications },
      })),
      reset: () => set(defaultPreferences),
    }),
    {
      name: 'user-preferences',
      version: 1,
      migrate: (persisted: any, version: number) => {
        if (version === 0) {
          return {
            ...defaultPreferences,
            ...persisted,
            notifications: {
              ...defaultPreferences.notifications,
              ...(persisted.notifications || {}),
            },
          }
        }
        return persisted
      },
    }
  )
)

购物车持久化 #

tsx
interface CartItem {
  productId: string
  name: string
  price: number
  quantity: number
  image: string
}

interface CartState {
  items: CartItem[]
  totalItems: number
  totalPrice: number
  
  addItem: (item: Omit<CartItem, 'quantity'>) => void
  removeItem: (productId: string) => void
  updateQuantity: (productId: string, quantity: number) => void
  clearCart: () => void
}

const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({
      items: [],
      totalItems: 0,
      totalPrice: 0,
      
      addItem: (item) => set((state) => {
        const existingItem = state.items.find(i => i.productId === item.productId)
        
        let newItems: CartItem[]
        if (existingItem) {
          newItems = state.items.map(i =>
            i.productId === item.productId
              ? { ...i, quantity: i.quantity + 1 }
              : i
          )
        } else {
          newItems = [...state.items, { ...item, quantity: 1 }]
        }
        
        return {
          items: newItems,
          totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0),
          totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
        }
      }),
      
      removeItem: (productId) => set((state) => {
        const newItems = state.items.filter(i => i.productId !== productId)
        return {
          items: newItems,
          totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0),
          totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
        }
      }),
      
      updateQuantity: (productId, quantity) => set((state) => {
        if (quantity <= 0) {
          return get().removeItem(productId)
        }
        
        const newItems = state.items.map(i =>
          i.productId === productId ? { ...i, quantity } : i
        )
        
        return {
          items: newItems,
          totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0),
          totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
        }
      }),
      
      clearCart: () => set({
        items: [],
        totalItems: 0,
        totalPrice: 0,
      }),
    }),
    {
      name: 'shopping-cart',
      partialize: (state) => ({
        items: state.items,
      }),
    }
  )
)

最佳实践 #

1. 不要持久化敏感数据 #

tsx
// ❌ 不好:持久化敏感数据
partialize: (state) => ({
  password: state.password,
  token: state.token,
})

// ✅ 好:排除敏感数据
partialize: (state) => {
  const { password, token, ...safe } = state
  return safe
}

2. 设置合理的版本号 #

tsx
// ✅ 好:使用版本号和迁移函数
{
  name: 'my-storage',
  version: 2,
  migrate: (persisted, version) => {
    // 迁移逻辑
  },
}

3. 处理存储失败 #

tsx
const safeStorage = {
  getItem: (name: string) => {
    try {
      return localStorage.getItem(name)
    } catch {
      return null
    }
  },
  setItem: (name: string, value: string) => {
    try {
      localStorage.setItem(name, value)
    } catch (error) {
      console.error('存储失败:', error)
    }
  },
  removeItem: (name: string) => {
    try {
      localStorage.removeItem(name)
    } catch (error) {
      console.error('删除失败:', error)
    }
  },
}

总结 #

状态持久化的关键点:

  • 使用 persist 中间件实现持久化
  • 选择合适的存储引擎
  • 使用 partialize 只持久化需要的状态
  • 使用 versionmigrate 处理版本升级
  • 使用 onRehydrateStorage 处理恢复后的逻辑

接下来,让我们学习 TypeScript 集成,掌握完整的类型支持。

最后更新:2026-03-28