状态持久化 #
为什么需要持久化? #
在 Web 应用中,我们经常需要将状态持久化存储,以便:
- 页面刷新后恢复状态
- 保存用户偏好设置
- 离线数据缓存
- 提升用户体验
使用 Atom Effects 实现持久化 #
localStorage 持久化 #
jsx
import { atom } from 'recoil';
const localStorageEffect = (key) => ({ setSelf, onSet }) => {
const savedValue = localStorage.getItem(key);
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
onSet((newValue, _, isReset) => {
isReset
? localStorage.removeItem(key)
: localStorage.setItem(key, JSON.stringify(newValue));
});
};
const themeState = atom({
key: 'theme',
default: 'light',
effects: [localStorageEffect('theme')],
});
sessionStorage 持久化 #
jsx
const sessionStorageEffect = (key) => ({ setSelf, onSet }) => {
const savedValue = sessionStorage.getItem(key);
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
onSet((newValue, _, isReset) => {
isReset
? sessionStorage.removeItem(key)
: sessionStorage.setItem(key, JSON.stringify(newValue));
});
};
const formDataState = atom({
key: 'formData',
default: {},
effects: [sessionStorageEffect('formData')],
});
通用持久化 Effect #
jsx
const persistenceEffect = (storage, key) => ({ setSelf, onSet }) => {
const savedValue = storage.getItem(key);
if (savedValue != null) {
try {
setSelf(JSON.parse(savedValue));
} catch (e) {
console.error(`Failed to parse stored value for ${key}:`, e);
}
}
onSet((newValue, _, isReset) => {
if (isReset) {
storage.removeItem(key);
} else {
try {
storage.setItem(key, JSON.stringify(newValue));
} catch (e) {
console.error(`Failed to store value for ${key}:`, e);
}
}
});
};
const themeState = atom({
key: 'theme',
default: 'light',
effects: [persistenceEffect(localStorage, 'theme')],
});
持久化多个状态 #
jsx
const userState = atom({
key: 'user',
default: null,
effects: [localStorageEffect('user')],
});
const settingsState = atom({
key: 'settings',
default: {
theme: 'light',
language: 'en',
notifications: true,
},
effects: [localStorageEffect('settings')],
});
const cartState = atom({
key: 'cart',
default: [],
effects: [localStorageEffect('cart')],
});
持久化复杂数据 #
处理日期 #
jsx
const dateEffect = (key) => ({ setSelf, onSet }) => {
const savedValue = localStorage.getItem(key);
if (savedValue != null) {
const parsed = JSON.parse(savedValue);
setSelf({
...parsed,
createdAt: new Date(parsed.createdAt),
});
}
onSet((newValue, _, isReset) => {
if (isReset) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(newValue));
}
});
};
处理 Map 和 Set #
jsx
const mapEffect = (key) => ({ setSelf, onSet }) => {
const savedValue = localStorage.getItem(key);
if (savedValue != null) {
const parsed = JSON.parse(savedValue);
setSelf(new Map(parsed));
}
onSet((newValue, _, isReset) => {
if (isReset) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify([...newValue]));
}
});
};
const setEffect = (key) => ({ setSelf, onSet }) => {
const savedValue = localStorage.getItem(key);
if (savedValue != null) {
const parsed = JSON.parse(savedValue);
setSelf(new Set(parsed));
}
onSet((newValue, _, isReset) => {
if (isReset) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify([...newValue]));
}
});
};
版本控制 #
jsx
const versionedEffect = (key, version = 1) => ({ setSelf, onSet }) => {
const storageKey = `${key}_v${version}`;
const savedValue = localStorage.getItem(storageKey);
if (savedValue != null) {
try {
setSelf(JSON.parse(savedValue));
} catch (e) {
console.error(`Failed to parse ${storageKey}:`, e);
localStorage.removeItem(storageKey);
}
}
onSet((newValue, _, isReset) => {
if (isReset) {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, JSON.stringify(newValue));
}
});
};
const settingsState = atom({
key: 'settings',
default: { theme: 'light' },
effects: [versionedEffect('settings', 2)],
});
迁移策略 #
jsx
const migrationEffect = (key, migrations) => ({ setSelf, onSet }) => {
let savedValue = localStorage.getItem(key);
if (savedValue != null) {
let data = JSON.parse(savedValue);
let version = data._version || 0;
while (version < migrations.length) {
data = migrations[version](data);
version++;
}
delete data._version;
setSelf(data);
}
onSet((newValue, _, isReset) => {
if (isReset) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify({
...newValue,
_version: migrations.length,
}));
}
});
};
const settingsState = atom({
key: 'settings',
default: { theme: 'light', fontSize: 16 },
effects: [
migrationEffect('settings', [
(data) => ({ ...data, fontSize: 16 }),
(data) => ({ ...data, language: 'en' }),
]),
],
});
加密存储 #
jsx
const encrypt = (data) => btoa(JSON.stringify(data));
const decrypt = (data) => JSON.parse(atob(data));
const encryptedEffect = (key) => ({ setSelf, onSet }) => {
const savedValue = localStorage.getItem(key);
if (savedValue != null) {
try {
setSelf(decrypt(savedValue));
} catch (e) {
console.error('Failed to decrypt:', e);
}
}
onSet((newValue, _, isReset) => {
if (isReset) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, encrypt(newValue));
}
});
};
const sensitiveState = atom({
key: 'sensitive',
default: {},
effects: [encryptedEffect('sensitive')],
});
IndexedDB 存储 #
jsx
const indexedDBEffect = (dbName, storeName, key) => ({ setSelf, onSet }) => {
const openDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName);
}
};
});
};
const getValue = async () => {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
};
const setValue = async (value) => {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(value, key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
};
getValue()
.then(value => {
if (value !== undefined) {
setSelf(value);
}
})
.catch(console.error);
onSet((newValue, _, isReset) => {
if (isReset) {
setValue(null).catch(console.error);
} else {
setValue(newValue).catch(console.error);
}
});
};
实战示例:用户设置持久化 #
jsx
import { atom, useRecoilState } from 'recoil';
const localStorageEffect = (key) => ({ setSelf, onSet }) => {
const savedValue = localStorage.getItem(key);
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
onSet((newValue, _, isReset) => {
isReset
? localStorage.removeItem(key)
: localStorage.setItem(key, JSON.stringify(newValue));
});
};
const userSettingsState = atom({
key: 'userSettings',
default: {
theme: 'light',
language: 'en',
fontSize: 16,
notifications: {
email: true,
push: true,
sms: false,
},
},
effects: [localStorageEffect('user_settings')],
});
function SettingsPanel() {
const [settings, setSettings] = useRecoilState(userSettingsState);
const updateTheme = (theme) => {
setSettings(prev => ({ ...prev, theme }));
};
const updateFontSize = (fontSize) => {
setSettings(prev => ({ ...prev, fontSize }));
};
const updateNotification = (key, value) => {
setSettings(prev => ({
...prev,
notifications: {
...prev.notifications,
[key]: value,
},
}));
};
return (
<div>
<h2>Settings</h2>
<div>
<label>Theme:</label>
<select
value={settings.theme}
onChange={(e) => updateTheme(e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div>
<label>Font Size: {settings.fontSize}px</label>
<input
type="range"
min="12"
max="24"
value={settings.fontSize}
onChange={(e) => updateFontSize(Number(e.target.value))}
/>
</div>
<div>
<h3>Notifications</h3>
<label>
<input
type="checkbox"
checked={settings.notifications.email}
onChange={(e) => updateNotification('email', e.target.checked)}
/>
Email
</label>
<label>
<input
type="checkbox"
checked={settings.notifications.push}
onChange={(e) => updateNotification('push', e.target.checked)}
/>
Push
</label>
</div>
</div>
);
}
总结 #
状态持久化的核心要点:
| 方式 | 适用场景 |
|---|---|
| localStorage | 长期存储,页面刷新保留 |
| sessionStorage | 会话存储,标签页关闭清除 |
| IndexedDB | 大量数据存储 |
| 自定义存储 | 服务器同步、加密存储 |
下一步,让我们学习 状态快照,了解状态快照与时间旅行。
最后更新:2026-03-28