状态持久化 #

为什么需要持久化? #

在 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