📚

備忘録: Reactの状態管理ライブライ"jotai"の非同期処理をCSRでもSSRでも問題なく利用できるようにする。

2024/06/13に公開

背景

jotaiでREST APIなど非同期のデータをfetchして利用する際、SSRの設定をしていると下記のエラーが発生する。

Error: Text content does not match server-rendered HTML.

useEffectなど駆使してレンダリング後に取得データをsetStateすれば解決するがすごく違和感あるからuseAtomしたらそのまま利用できるようにしたい。
一応公式でもAsyncの対応やSSRの対応についても言及しているが、どれもうまくいかなかった。
諦めてCSRで実装したがそうするとatomWithRefreshが利用できず、ステートの更新に苦心する。
もう自分で理想のAtomを実装するしかない。
というわけで早速結論です。

結論

データの読み込み時のステータス管理はloadableを参考に実装。

import { atom } from "jotai";

// 処理の状態を表す汎用的な型
type AsyncStatus<T> = {
  state: "loading" | "hasData" | "hasError";
  data?: T;
  error?: Error;
};

// 初期状態を設定するための関数
function createInitialState<T>(): AsyncStatus<T> {
  return {
    state: "loading",
    data: undefined,
    error: undefined,
  };
}

// 汎用的なアトムを作成する関数
export const asyncAtom = <T>(fetchFunction: () => Promise<T>) => {
  const initialState = createInitialState<T>();
  const dataAtom = atom<AsyncStatus<T>>(initialState);

  const refreshAtom = atom(
    (get) => get(dataAtom),
    async (_, set) => {
      set(dataAtom, { state: "loading", data: undefined, error: undefined });

      try {
        const data = await fetchFunction();
        set(dataAtom, {
          state: "hasData",
          data: data,
          error: undefined,
        });
      } catch (error) {
        set(dataAtom, { state: "hasError", data: undefined, error });
      }
    }
  );

  return refreshAtom;
};

使い方は下記

// 適当にAPIからデータを取得する処理
const fetchData = async () => {
  const response = await fetch("https://api.example.com/data");
  if (!response.ok) {
    throw new Error("データの取得に失敗しました");
  }
  return response.json();
};

// Atomを定義
const dataAtom = asyncAtom(fetchData);

// Atomを利用してUIを表示
const SampleComponent: React.FC = () => {

  const [data, refresh] = useAtom(asyncAtom(fetchSampleData));

  React.useEffect(() => {
    refresh();
  }, [refresh]);

  return (
    <div>
      {data.state === "loading" && <p>Loading...</p>}
      {data.state === "hasError" && <p>Error: {status.error?.message}</p>}
      {data.state === "hasData" && (
        <pre>{JSON.stringify(data.data, null, 2)}</pre>
      )}
      <button onClick={refresh}>Refresh</button>
    </div>
  );
};

非常にシンプルにjotaiで非同期処理を扱えるようになったと思う。
特にログイン中のユーザデータを扱う時に利用したいと考えている。
FlutterのRiverpodだとこのあたりあまり意識してなかったが、SSRはまた違った考慮項目があって勉強になった。

最後に

もっと良い方法などあればぜひ教えてください!

Discussion