かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React 👻 jotai を使うと localStorage を使った永続化が簡単だった件について

SPA はリロードすると state が消えてしまうので永続化したい状態ってのが結構あります
生の React だと カスタム hook や useEffect 内で localStorage にアクセスして保存させたえい、Recoil でも effect を使って永続化するコードを書く必要がありました
jotai を使うと localStorage に保存するための API が用意されていて永続化がめちゃくちゃ簡単だったのでメモに残しておきます

環境

  • React 18.2.0
  • Next.js 13.2.4
  • jotai 2.0.3
  • TypeScript 5.0.2

atomWithStorage で直接 localStorage に state を保存できる!

atomWithStorage(key, initialValue, storage)

  • key (required): a unique string used as the key when syncing state with localStorage, sessionStorage, or AsyncStorage
  • initialValue (required): the initial value of the atom
  • storage (optional): an object with:
    • getItem, setItem and removeItem methods for storing/retrieving/deleting persisted state; defaults to using localStorage for storage/retrieval and JSON.stringify()/JSON.parse() for serialization/deserialization.
    • Optionally, the storage has a subscribe property, which can be used to synchronize storage. The default localStorage handles storage events for cross-tab synchronization.

cf. Storage — Jotai, primitive and flexible state management for React

auth.atom.ts

import { atomWithStorage } from 'jotai/utils';
export const isLoggedIn = true as const;
export const unLoggedIn = false as const;
const beforeLoginCheck = undefined;
type AuthType = typeof isLoggedIn | typeof unLoggedIn | typeof beforeLoginCheck;
// "auth" という key で localStorage に保存される
// 保存される値は自動的に JSON.stringify() される
export const authAtom = atomWithStorage<AuthType>('auth', beforeLoginCheck);

localStorage に auth という key で保存する state を作成

useAuth.ts

import { useCallback } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { RESET } from 'jotai/utils';
import { authAtom, isLoggedIn, unLoggedIn } from './auth.atom';

export const useAuth = () => {
  return useAtomValue(authAtom);
};

export const useAuthMutators = () => {
  const setAuth = useSetAtom(authAtom);
  
  const clearAuth = useCallback(() => {
    // RESET を渡すと localStorage から削除される
    setAuth(RESET);
  }, [setAuth]);

  const login = useCallback(() => {
    setAuth(isLoggedIn);
  }, [setAuth]);

  const loginError = useCallbacl(() => {
    setAuth(unLoggedIn);
  }, [setAuth]);
  
  return {clearAuth, login, loginError};
};

useAuth() は localStorage から取り出した値を返すだけの hook。
※ localStorage に値が存在しない場合は atom 作成時の initialValue が返される

useAuthMutators() は localStorage から delteItem で永続化した state を完全に削除しうてしまう clearAuth と 値を更新して localStorage に保存する login と loginError を提供するようにした

コンポーネントでの使用
import { useCallback } from 'react';
import { useAuthMutators } from './useAuth';

function LogoutButton(): JSX.Element {
  const { clearAuth } = useAuthMutators();
  const handleLogout = useCallback(() => {
    clearAuth()
  }, [clearAuth]);

  return <button type="button" onClick={handleLogout}>Logout</button>
}

function LoginForm(): JSX.Element {
  const { login, loginError } = useAuthMutators();
  const handleSubmit = useCallback(async (evt: FormEvent<HTMLFormElement>) => {
    evt.preventDefault();
    // 略
    try {
      const isLogin = await postLoginForm(data);
      login();
    } catch (error) {
      loginError();
    }
  }, [login, loginError]);

  return <form onSubmit={handleSubmit}>{/*略*/}</form>;
}

export { LogoutButton, LoginForm };

👇

import { useAuth } from './useAuth';
import { LoginForm, LogoutButton } from './MyComponents';

function MyApp(): JSX.Element {
  const auth = useAuth();  
  if ( !auth ) {
    return <LoginForm />;
  }
  
  return <LogoutButton />;
}

ログインに成功すると localStorage に {"auth": "true"} が保存され、ログアウトすると localStorage から state が削除される

jotai atomWithStorage sample

所管

jotai を使えば localStorage にアクセスする部分を意識することなく localStorage で状態の永続化ができました!
また、リロードの瞬間など localStorage にアクセスできない時は atom 作成時の initialValue が返されるようになっていて Next.js で使う場合も localStorage にアクセスできないサーバーサイドを意識することなくそのまま使うことができました
initialValue を工夫したり <Suspense> と併用することでリロード時だけ loader を挟むなどもできそうです!

👻 jotai のメモ


[参考]