FumadocsZDecode
React

数据持久化

在 React 应用中,页面刷新会导致状态丢失。通过浏览器存储、URL 状态或状态管理库,可以实现数据的持久化保存,保证用户体验连贯。


一、主流持久化方案

方案存储空间适用场景特点
localStorage5-10MB用户偏好、表单数据永久保存
sessionStorage5-10MB临时会话数据关闭页面清除
URL 参数受浏览器限制筛选条件、页面状态可分享、可收藏
Redux Persist由存储方式决定复杂全局状态自动持久化

二、localStorage 方案

最常用的本地存储方案,适合保存用户偏好和表单草稿。

基础用法:状态初始化和自动保存

function TodoList() {
  const [todos, setTodos] = React.useState<string[]>(() => {
    // 组件挂载时从 localStorage 读取
    const saved = localStorage.getItem('todos')
    return saved ? JSON.parse(saved) : []
  })

  // 状态变化时自动保存
  React.useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos))
  }, [todos])

  const addTodo = (text: string) => {
    setTodos([...todos, text])
  }

  return (
    <div>
      <ul>
        {todos.map((todo, i) => (
          <li key={i}>{todo}</li>
        ))}
      </ul>
      <button onClick={() => addTodo('新任务')}>添加</button>
    </div>
  )
}

防抖保存(避免频繁写入)

function AutoSaveForm() {
  const [formData, setFormData] = React.useState(() => {
    const saved = localStorage.getItem('formData')
    return saved ? JSON.parse(saved) : { title: '', content: '' }
  })

  React.useEffect(() => {
    // 防抖:1秒无操作后才保存
    const timer = setTimeout(() => {
      localStorage.setItem('formData', JSON.stringify(formData))
    }, 1000)

    return () => clearTimeout(timer)
  }, [formData])

  return (
    <div>
      <input
        value={formData.title}
        onChange={(e) => setFormData({ ...formData, title: e.target.value })}
        placeholder="标题"
      />
      <textarea
        value={formData.content}
        onChange={(e) => setFormData({ ...formData, content: e.target.value })}
        placeholder="内容"
      />
    </div>
  )
}

错误处理

function getStoredData<T>(key: string, defaultValue: T): T {
  try {
    const item = localStorage.getItem(key)
    return item ? JSON.parse(item) : defaultValue
  } catch (error) {
    console.error(`Failed to read ${key} from localStorage:`, error)
    return defaultValue
  }
}

function MyComponent() {
  const [data, setData] = React.useState(() =>
    getStoredData('myData', { count: 0 }),
  )

  return <div>{data.count}</div>
}

三、URL 状态

适合保存筛选条件、搜索参数等可共享的页面状态。

import { useSearchParams } from 'react-router-dom'

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams()

  // 从 URL 读取搜索词
  const keyword = searchParams.get('keyword') || ''
  const category = searchParams.get('category') || 'all'

  const handleSearch = (value: string) => {
    setSearchParams({ keyword: value, category })
  }

  const handleCategoryChange = (value: string) => {
    setSearchParams({ keyword, category: value })
  }

  return (
    <div>
      <input
        value={keyword}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="搜索"
      />
      <select
        value={category}
        onChange={(e) => handleCategoryChange(e.target.value)}
      >
        <option value="all">全部</option>
        <option value="articles">文章</option>
        <option value="users">用户</option>
      </select>
    </div>
  )
}

四、Redux Persist 方案

结合 Redux 实现复杂状态的自动持久化。

import { configureStore } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { rootReducer } from './slices'

const persistConfig = {
  key: 'root',
  storage,
  // 只持久化指定的状态片段
  whitelist: ['user', 'settings'],
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

export const store = configureStore({
  reducer: persistedReducer,
})

export const persistor = persistStore(store)

在应用入口使用 PersistGate 包裹:

import { PersistGate } from 'redux-persist/integration/react'
import { store, persistor } from './store'

function App() {
  return (
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <MainApp />
      </PersistGate>
    </Provider>
  )
}

五、跨标签页同步

同一浏览器的不同标签页可以通过 storage 事件同步数据。

function SyncedComponent() {
  const [userData, setUserData] = React.useState(() =>
    getStoredData('userData', null),
  )

  React.useEffect(() => {
    const handleStorageChange = (e: StorageEvent) => {
      // 监听其他标签页的 localStorage 变化
      if (e.key === 'userData' && e.newValue) {
        setUserData(JSON.parse(e.newValue))
      }
    }

    window.addEventListener('storage', handleStorageChange)
    return () => window.removeEventListener('storage', handleStorageChange)
  }, [])

  return <div>{userData?.name}</div>
}

六、数据有效性检查

某些场景下需要检查存储的数据是否过期。

interface CachedData {
  value: any
  timestamp: number
  ttl: number // 生存时间(毫秒)
}

function isCacheValid(cached: CachedData): boolean {
  return Date.now() - cached.timestamp < cached.ttl
}

function getOrFetchData(key: string, fetcher: () => Promise<any>) {
  const cached = getStoredData<CachedData | null>(key, null)

  if (cached && isCacheValid(cached)) {
    return cached.value
  }

  // 缓存过期或不存在,重新获取
  return fetcher().then((data) => {
    localStorage.setItem(
      key,
      JSON.stringify({
        value: data,
        timestamp: Date.now(),
        ttl: 3600000, // 1 小时
      }),
    )
    return data
  })
}

核心选择:简单数据用 localStorage(防抖处理避免频繁写入),可分享的状态用 URL 参数,复杂全局状态用 Redux Persist,跨标签页需要同步时监听 storage 事件,有失效需求时加时间戳和 TTL 检查。

On this page