kk-web

SWRの使いドコロ

2022-06-29

よく SWR を使って実装を行うのですが。

公式サイト はおしゃれで情報もよくまとまっているのですが、そのわりに初心者には分かりづらいという、なんとも不思議な感じだったりします。

ということで、普段自分が使用している範囲の SWR の書きっぷりを書いていこうと思います。


クライアント側でデータを取得する

たまに勘違いしている人がいますが、SWR って要するにクライアント側で fetcher を叩いてくれるライブラリでしかありません。

基本的な部分は useEffect で fetcher を叩くのとなんら変わりません。

import axios, { AxiosResponse } from "axios";

async function fetcher(url: string): Promise<AxiosResponse["data"]> {
  const { data } = await axios.get(url);

  return data;
}

export default fetcher;

fetcher は以下すべて同じため省略します。

import useSWR from "swr";

function HogePresentationalComponent(): JSX.Element {
  // 開発環境では基本的に localhost:3000 へのアクセスとなる
  // つまり同じドメインへの api のコールとなる
  const { data } = useSWR<GetTherapistUsersData>("/api/hoge", fetcher);

  return <div>{data}</div>;
}

return HogePresentationalComponent;

たまに useEffect で state を準備するのが面倒だからといって SWR を導入しているプロジェクトがありますが。

初期値で SWR を使用すると、思いがけないタイミング で再度 fetcher が叩かれるケースもあるので注意が必要です。

先にサーバー側で初期データを取得する

Next.js の場合は getServerSideProps や getStaticProps などを使用すると、レンダリング前にデータの取得を行うことが可能です。

SWR のみを使用した場合、レンダリング後に fetcher が叩かれるため、一瞬ブランクな状態が発生してしまいます。

もちろんローディング用の state も提供されているため、仕様次第ではありますが。

せっかくならブランクな状態を避けてデータを表示したいですよね。

ということで、以下のような実装が加えられます。

import { SWRConfig } from "swr";

export type HogeContainerComponentProps = {
  fallback: {
    "/api/hoge": Hoge;
  };
};

function HogeContainerComponent({
  fallback,
}: HogeContainerComponentProps): JSX.Element {
  return (
    <SWRConfig value={{ fallback }}>
      <HogePresentationalComponent />
    </SWRConfig>
  );
}

// getStaticProps でも同様
export const getServerSideProps: GetServerSideProps<
  HogeContainerComponentProps
> = async () => {
  const { data: hoge } = await axios.get("http://hogehoge/api/hoge");

  return {
    props: {
      fallback: {
        "/api/hoge": hoge,
      },
    },
  };
};

return HogeContainerComponent;

で、この実装のユニークなところが、Presentational Component 側は props を通して初期データを取得しているわけではないというところです。

おそらく SWR の内部的に Context などを使用して初期データを渡しているんだろうなーと思われます。

POST や PATCH を叩いて描画を更新する

よくある仕様の 1 つに、データの更新を行った後、画面の描画を更新したいケースが挙がると思いますが。

SWR などを使用しない場合、基本的にはページのリロードが必要になります。

export type HogeContainerComponentProps = {
  hoge: Hoge;
};

function HogeContainerComponent({
  hoge,
}: HogeContainerComponentProps): JSX.Element {
  const router = useRouter();
  const handleSubmit = useCallback(async (values) => {
    await axios.patch("/api/hoge", values);

    // router.push("/fuga") のようなケースも含む
    router.reload();
  }, []);

  return <HogePresentationalComponent hoge={hoge} onSubmit={handleSubmit} />;
}

export const getServerSideProps: GetServerSideProps<
  HogeContainerComponentProps
> = async () => {
  const { data: hoge } = await axios.get("http://hogehoge/api/hoge");

  return {
    props: {
      hoge,
    },
  };
};

return HogeContainerComponent;

この場合、ページのリロード(または画面遷移)が発生し、再度サーバー側で api の呼び出しが行われるため、描画がもたつきます。

で、これを回避するのために、SWR では以下の 2 つの方法が準備されています。

  1. データの更新を行い、クライアント側で fetcher を叩き直し、描画を更新する
  2. データで描画を更新し、データの更新を行い、クライアント側で fetcher を叩き直す

データの更新を行い、クライアント側で fetcher を叩き直し、描画を更新する

まずは簡単な方法です。

import useSWR, { useSWRConfig } from "swr";

export type HogeContainerComponentProps = {
  fallback: {
    "/api/hoge": Hoge;
  };
};

function HogeContainerComponent({
  fallback,
}: HogeContainerComponentProps): JSX.Element {
  const { mutate } = useSWRConfig();
  const handleSubmit = useCallback(async (values) => {
    await axios.patch("/api/hoge", values);

    // 遷移する場合は router.push("/fuga", undefined, { shallow: true }) と書くことができます
    // このためサーバー側へのアクセスを省略でき、描画の高速化が実現できます
    mutate("/api/hoge");
  }, []);

  return (
    <SWRConfig value={{ fallback }}>
      <HogePresentationalComponent onSubmit={handleSubmit} />
    </SWRConfig>
  );
}

// getStaticProps でも同様
export const getServerSideProps: GetServerSideProps<
  HogeContainerComponentProps
> = async () => {
  const { data: hoge } = await axios.get("http://hogehoge/api/hoge");

  return {
    props: {
      fallback: {
        "/api/hoge": hoge,
      },
    },
  };
};

return HogeContainerComponent;

実装はめちゃくちゃ楽ですが、この場合、サーバー側でデータの更新が行われた後、再度取得し直してようやく描画が更新されるため、リアルタイム性には劣ります。

データで描画を更新し、データの更新を行い、クライアント側で fetcher を叩き直す

こちらは実装に多少手がかかりますが、即時描画が更新されます。

ただ Container / Presentational Component の考え方とは相性が良くないため、実装は省略します。

公式サイト を見ていただければその理由がわかると思います。

また、このケースはローカルのデータが正しいこと前提で描画を行うため、多少リスキーな部分もあります。

ページネーションやフィルタリングを組む

ページネーションやフィルタリングには SWR が抜群に噛み合います。

import useSWR from "swr";

function HogePresentationalComponent(): JSX.Element {
  const [pageIndex, setPageIndex] = useState(0);
  const { data } = useSWR<GetTherapistUsersData>(
    `/api/hoge?page=${pageIndex}`,
    fetcher,
  );

  return (
    <div>
      <div>{data}</div>
      <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
    </div>
  );
}

return HogePresentationalComponent;

最初にアクセスされたページのみサーバー側で取得して、その後のページャーを通したページ遷移はすべてクライアント側で済ませることが可能となります。


大体こんなものかなーと思うのですが、いかがでしょうか。

あとは無限スクロールとかも手軽に実装可能です、ここまでくるとリッチな UX だなーと感じますよね。

最後に、自身の過去の経験上、SWR を使用する際に勘違いしやすいポイントとしては以下のような感じかなーと思います。

  • fecther はコンポーネントの外側で定義する、useCallback で作成した関数を割り当てないようにすること
  • fetcher は基本的に同一となる
  • SWR から呼び出す api は同じドメインが一般的
  • オプションを正しく把握していないと、予期せぬタイミングで api が叩かれることになる
  • getStaticProps に限らず getServerSideProps で使用しても恩恵は得られる(これは公式の書きっぷりがイマイチだと思います)
  • データの更新時、即時で画面描画を更新したい場合、コンポーネント設計によってはトリッキーな実装が求められる
  • データの更新時、即時で画面描画を更新する場合、データの正しさが保証されていない状態で描画されてしまうため注意が必要
  • api の response header へは基本的にアクセスできない

そんな感じです。

公式だとチャットの実装がサンプルとして挙がっていますが、こういったケースで実装することは少ないと思います。

どなたかの参考になれば幸いです。

© 2018 kk-web