前端开发中,相信大家都遇到过这样的痛点:页面一刷新,辛辛苦苦填的表单数据没了!精心选择的筛选条件重置了!
今天我们就来深入探讨React中数据持久化的5大核心方案,从简单到复杂,帮你彻底解决这个难题!
一、问题场景:为什么数据会丢失?
我们先通过一个简单例子感受一下问题:
php
function UserForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
preferences: {
theme: 'light',
notifications: true
}
});
// 用户输入数据...
// 页面刷新 → 所有数据归零 😭
return (
<form>
<input
value={formData.username}
onChange={(e) => setFormData({...formData, username: e.target.value})}
/>
{/* 其他表单项 */}
</form>
);
}
数据丢失的根本原因是:React状态(state)存储在内存中,页面刷新时内存被清空。
二、方案一:localStorage(最常用)
基本实现
javascript
function useLocalStorage(key, initialValue) {
// 从localStorage读取初始值
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// 更新状态并保存到localStorage
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// 使用示例
function PersistentForm() {
const [userData, setUserData] = useLocalStorage('user_form_data', {
username: '',
email: '',
theme: 'light'
});
return (
<div>
<input
value={userData.username}
onChange={(e) => setUserData({
...userData,
username: e.target.value
})}
/>
{/* 保存后刷新页面,数据还在! */}
</div>
);
}
进阶:自动过期和加密
typescript
class SecureStorage {
constructor(namespace) {
this.namespace = namespace;
}
set(key, value, ttl = null) {
const item = {
value,
timestamp: Date.now(),
ttl
};
// 简单加密(生产环境应使用更安全的方案)
const encrypted = btoa(JSON.stringify(item));
localStorage.setItem(`${this.namespace}:${key}`, encrypted);
}
get(key) {
const encrypted = localStorage.getItem(`${this.namespace}:${key}`);
if (!encrypted) return null;
try {
const item = JSON.parse(atob(encrypted));
// 检查是否过期
if (item.ttl && Date.now() - item.timestamp > item.ttl) {
this.remove(key);
return null;
}
return item.value;
} catch {
return null;
}
}
}
// 使用
const storage = new SecureStorage('myApp');
storage.set('token', 'abc123', 1000 * 60 * 60); // 1小时过期
三、方案二:sessionStorage(标签页级别)
javascript
function useSessionStorage(key, initialValue) {
const [state, setState] = useState(() => {
// 只在客户端执行
if (typeof window === 'undefined') return initialValue;
const stored = sessionStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
sessionStorage.setItem(key, JSON.stringify(state));
}, [key, state]);
return [state, setState];
}
// 特性:不同标签页数据隔离,关闭标签页自动清除
四、方案三:IndexedDB(大量数据)
当需要存储大量结构化数据时,IndexedDB是更好的选择:
ini
class IndexedDBService {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('state')) {
db.createObjectStore('state', { keyPath: 'key' });
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve();
};
request.onerror = (event) => reject(event.target.error);
});
}
async set(key, value) {
const transaction = this.db.transaction(['state'], 'readwrite');
const store = transaction.objectStore('state');
store.put({ key, value });
}
async get(key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['state'], 'readonly');
const store = transaction.objectStore('state');
const request = store.get(key);
request.onsuccess = () => resolve(request.result?.value);
request.onerror = () => reject(request.error);
});
}
}
// React Hook封装
function useIndexedDB(key, initialValue) {
const [value, setValue] = useState(initialValue);
const [db, setDb] = useState(null);
useEffect(() => {
const initDB = async () => {
const dbService = new IndexedDBService('AppDB', 1);
await dbService.open();
setDb(dbService);
const stored = await dbService.get(key);
if (stored) setValue(stored);
};
initDB();
}, [key]);
const updateValue = async (newValue) => {
setValue(newValue);
if (db) await db.set(key, newValue);
};
return [value, updateValue];
}
五、方案四:URL参数(分享友好型)
适合筛选条件等需要分享的场景:
ini
import { useHistory, useLocation } from 'react-router-dom';
function useURLState(key, initialValue) {
const location = useLocation();
const history = useHistory();
const queryParams = new URLSearchParams(location.search);
const [value, setValue] = useState(() => {
const param = queryParams.get(key);
return param ? JSON.parse(decodeURIComponent(param)) : initialValue;
});
const setURLValue = (newValue) => {
setValue(newValue);
// 更新URL参数
const newParams = new URLSearchParams(location.search);
newParams.set(key, encodeURIComponent(JSON.stringify(newValue)));
history.replace({
pathname: location.pathname,
search: newParams.toString()
});
};
return [value, setURLValue];
}
// 使用:URL会显示 ?filters=%7B%22status%22%3A%22active%22%7D
function FilterComponent() {
const [filters, setFilters] = useURLState('filters', { status: 'all' });
// 分享当前URL,别人打开能看到同样的筛选结果
}
六、方案五:Redux + 持久化中间件
javascript
// store/persistConfig.js
const persistConfig = {
key: 'root',
storage, // 可以是localStorage、sessionStorage等
whitelist: ['auth', 'userPreferences'], // 只持久化这些reducer
blacklist: ['temporaryData'], // 不持久化这些
transforms: [
// 数据转换(如加密)
encryptTransform({
secretKey: process.env.REACT_APP_ENCRYPT_KEY
})
]
};
// store/index.js
import { createStore } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = createStore(persistedReducer);
export const persistor = persistStore(store);
// App.js
import { PersistGate } from 'redux-persist/integration/react';
function App() {
return (
<Provider store={store}>
<PersistGate loading={<LoadingSpinner />} persistor={persistor}>
<MainApp />
</PersistGate>
</Provider>
);
}
七、数据持久化流程图
小量简单数据
标签页隔离数据
大量结构化数据
可分享状态
全局状态管理
页面刷新数据恢复决策localStoragesessionStorageIndexedDBURL参数Redux持久化快速读取
5-10MB限制会话级存储
标签页独立大容量存储
异步操作可分享状态
URL可见集成Redux
状态同步数据恢复完成
八、性能与安全最佳实践
1. 防抖保存避免频繁写入
javascript
function useDebouncedPersist(key, value, delay = 1000) {
useEffect(() => {
const timer = setTimeout(() => {
localStorage.setItem(key, JSON.stringify(value));
}, delay);
return () => clearTimeout(timer);
}, [key, value, delay]);
}
2. 数据压缩(大数据量时)
javascript
import { compress, decompress } from 'lz-string';
const compressData = (data) => {
return compress(JSON.stringify(data));
};
const decompressData = (compressed) => {
return JSON.parse(decompress(compressed));
};
3. 安全存储敏感信息
javascript
// 永远不要这样做!
localStorage.setItem('token', rawToken);
// 应该:使用HttpOnly Cookie或临时内存存储
九、方案对比选择指南
| 方案 | 容量 | 生命周期 | 使用场景 | 优缺点 |
|---|---|---|---|---|
| localStorage | 5-10MB | 永久 | 用户偏好设置 | 简单易用,但同步阻塞 |
| sessionStorage | 5-10MB | 标签页 | 临时表单数据 | 标签页隔离,关闭即失 |
| IndexedDB | 大量 | 永久 | 离线应用数据 | 异步大容量,API复杂 |
| URL参数 | 受限 | 页面级别 | 筛选条件分享 | 可分享,但有长度限制 |
| Redux持久化 | 灵活 | 可配置 | 全局状态管理 | 集成度高,需额外配置 |
十、实战:完整表单持久化示例
javascript
function PersistentForm() {
// 综合使用多种方案
const [basicInfo, setBasicInfo] = useLocalStorage('form_basic', {});
const [tempData, setTempData] = useSessionStorage('form_temp', {});
const [filters, setFilters] = useURLState('form_filters', {});
// 自动保存草稿
useDebouncedPersist('form_draft', {
basicInfo,
tempData
}, 2000);
// 离开页面提示
useBeforeUnload(() => {
if (hasUnsavedChanges) {
return '您有未保存的更改,确定要离开吗?';
}
});
return (
<div>
{/* 表单内容 */}
<button onClick={clearAllStorage}>
清空所有持久化数据
</button>
</div>
);
}
总结
React数据持久化没有银弹,关键在于根据场景选择合适的方案:
-
- 用户设置 → localStorage
-
- 临时表单 → sessionStorage
-
- 离线数据 → IndexedDB
-
- 分享状态 → URL参数
-
- 全局状态 → Redux持久化
记住一个原则:敏感信息永远不要在前端持久化!token、密码等应该通过HttpOnly Cookie或后端session管理。