持久化 atom

在默认情况下,所有的 atom 都是保存在内存中的,一旦离开页面或者页面发生刷新,atom 的内容就恢复成初始值了。但是在实际使用中往往会对一些数据存在持久化的需求。对于浏览器来说,持久化的数据一般都是放置在localStoragesessionStorage或者是IndexedDB中。

如果在一个 atom 的 read 函数和 write 函数中编写访问localStorage的功能,固然是可以实现将 atom 的内容持久化到localStorage的功能,但是对于原子化的状态来说,可能需要持久化的 atom 有数个,逐一编写相同的功能非常不现实也不明智。所以此时就需要用到jotai/utils中提供的atomWithStorage函数来提供可以将内部数据保存到localStorage或者sessionStorage能力的 atom。

atomWithStorage函数接受四个参数:

  1. key,这是 atom 的内容在持久化存储中使用的键值,构建 atom 时必须提供。
  2. initialValue,这是 atom 内容的初始化值,构建 atom 时必须提供。
  3. storage,要持久化到的 Storage 的实现,构建 atom 时不必提供,在不提供的时候 Jotai 会默认将 atom 的内容持久化到localStorage
  4. options,用于提供给atomWithStorage的选项,构建 atom 时不必提供。options时一个对象,可以使用以下内容对 atom 的运作特性进行配置。
    • getOnInit,一个布尔类型的值,在不设置或者设置为false的时候,表示 atom 在初始化的时候只会返回初始值,不会主动去存储中获取值。如果更希望 atom 积极从存储中获取值,可以将此项配置设置为true

所以atomWithStorage最简单的使用方式就是直接提供keyinitialValue两个参数即可。

以下模拟一个将主题配置保存到localStorage的示例。

const themeAtom = atomWithStorage("theme", "dark");
// 如果想积极从 localStorage 中读取值,可以使用以下方式定义 atom
const themeAtom = atomWithStorage("theme", "dark", undefined, {
  getOnInit: true,
});

const App = () => {
  const [theme, setTheme] = useAtom(themeAtom);

  return (
    <div data-theme={theme}>
      {theme === "light" && (
        <button onClick={() => setTheme("dark")}>Dark</button>
      )}
      {theme === "dark" && (
        <button onClick={() => setTheme("light")}>Light</button>
      )}
    </div>
  );
};

在这个示例中可以看出,支持持久化的 atom 在使用的时候与普通的 atom 没有任何两样,在对 atom 进行写入操作的时候,atom 会自动将其持有的内容写入相应的存储中。

持久化内容的删除

定义好的 atom 在使用set方法设置其值的时候,无论设置为空白字符串或者是null,都表示在localStorage里存在一个值。但是有一些情况下我们的确需要将存储中的内容删去。这时就需要利用到jotai/utils中定义的RESET信号值了。

例如在上面的示例中调用setTheme(RESET)就可以将localStorage中的键为theme的记录删去。

自定义 Storage

Jotai 默认提供的存储实现是localStorage,如果需要使用其他的存储实现,那么就需要自行动手实现或者寻找其他人已经完成的实现。

在原理上,要使用一个自定义的存储,只需要在定义 atom 的时候传入第三个参数来指定要使用的storage即可。这个storage参数的定义在 Jotai 里也有非常明确的定义。任何一个storage参数都必须提供以下几个方法。

  1. getItem(key, initialValue),用于从存储中获取键对应的值,如果键不存在,将返回initialValue,这个方法是必须提供的。
  2. setItem(key, value),用于将指定值放入存储的对应键,这个方法也是必须提供的。
  3. removeItem(key),用于从存储中删除指定的键,这个方法也是必须提供的。
  4. subscribe(key, callback, initialValue),用于订阅存储更新的方法,这个方法是可选的。

从上面这四个需要提供的方法上看,实现一个自定义的storage还是十分容易的,不过 Jotai 提供了另外一个工具函数createJSONStorage,如果是以 JSON 格式存储数据,那么可以更方便的完成storage的自定义。

createJSONStorage的函数签名如下:

function createJSONStorage<Value>(
  getStringStorage: () => AsyncStringStorage | SyncStringStorage,
  options?: {
    reviver?: (key: string, value: unknown) => unknown;
    replacer?: (key: string, value: unknown) => unknown;
  }
): AsyncStorage<Value> | SyncStorage<Value>;

createJSONStorage的函数签名中所出现的AsyncStringStorageSyncStringStorage就是上文中所提到的实现了四种指定方法的使用字符串进行数据存储的storage,默认使用的是window.localStorageoptions里的reviver是用来将字符串信息转换为对象的,默认实现是JSON.parsereplacer是用来将一个对象转换为字符串的,默认实现为JSON.stringify

如果要使用其他字符串以外的格式存储数据,那么就不能使用createJSONStorage来进行storage的自定义了。