12周年記念パーティ開催! 2024/5/10(金) 19:00

SWRで快適! Reactでのデータ取得 第1回 Reactにおけるデータ取得の基本とSWR

一口にデータ取得と言っても、キャッシュの保持とその破棄や、最新データの再取得、エラーが返ってきた場合など、さまざまなことを考慮すると面倒なものです。そんなとき、SWRというライブラリが便利に使えます。

発行

著者 杉浦 有右嗣 シニアエンジニア
SWRで快適! Reactでのデータ取得 シリーズの記事一覧

はじめに

このシリーズでは、データ取得のためのReact Hooksを提供するライブラリ、SWRについて解説していきます。

SWRは、Next.jsを開発しているVercel社のOSSであり、同じくVercelのGitHubにてソースコードが公開されています。

このサイトにもあるように、"SWR"とは、stale-while-revalidateというキャッシュ戦略の略称です。この意味についてもシリーズ中で追って解説していく予定ですが、まずはライブラリとしての使い方を確認していきましょう。

Reactにおけるデータ取得のおさらい

その前に、まずはReactのコードにおけるデータ取得の基本的なコードをおさらいしておきます。

Reactの基本的な使い方や、React Hooksについては、すでに一定の知識がある前提で話を進めていきます。もし不明な点があれば、別のシリーズ記事を参照してください。

次のコードは、「画面に表示されたとき、HackerNews APIへリクエストを発行し、取得したデータを表示するコンポーネント」です。記事一覧画面からいずれかの記事を選んで表示される、記事詳細画面のコンポーネントだと思ってください。

記事詳細画面でデータを表示するコンポーネント

const fetcher = (url) =>
  fetch("https://hacker-news.firebaseio.com/v0" + url).then((res) =>
    res.json()
  );

const NewsDetail = ({ id }) => {
  const [data, setData] = useState(null);
  useEffect(() => {
    setData(null);
    fetcher(`/item/${id}.json`)
      .then((item) => setData(item));
  }, [id]);

  if (!data) return <p>Loading...</p>;

  return (
    <div>
      <h2>{data.title}</h2>
      <div style={{ display: "flex", gap: 8 }}>
        <span>📝 {data.by}</span>
        <span>🅿️ {data.score}</span>
        <span>💬 {data.descendants}</span>
      </div>
    </div>
  );
};

useState()フックに記事データを保持するようにし、useEffect()フックでAPIを呼び出してデータを取得する基本的なコードです。

このコンポーネントは、Propsで記事のidを受け取り、取得したいデータのURLを決定しているため、useEffect()フックの依存配列にもidを追加しています。

正常系だけであればこれで問題なく表示できますが、実際にはデータが取得できずエラーになった場合なども考慮する必要がありますので、その点を考慮して次のように修正します。

エラー時に情報を保持することを考慮

const NewsDetail = ({ id }) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null); // 追加
  useEffect(() => {
    setData(null);
    setError(null); // 追加
    fetcher(`/item/${id}.json`)
      .then((item) => setData(item))
      .catch((err) => setError(err)); // 追加
  }, [id]);

  if (error) return <p>Error: {error.message}</p>; // 追加
  if (!data) return <p>Loading...</p>;

  return (
    <div>
      <h2>{data.title}</h2>
      <div style={{ display: "flex", gap: 8 }}>
        <span>📝 {data.by}</span>
        <span>🅿️ {data.score}</span>
        <span>💬 {data.descendants}</span>
      </div>
    </div>
  );
};

エラー時にその情報を保持するため、新たにuseState()フックを追加しました。そして、必要に応じてそれらを表示します。

このように、「単にAPIへリクエストを発行し、そのデータを表示する」という要件を実現するだけでも、それなりのコードが必要になってしまいました。

他のコンポーネントでも同様の処理をすることがあると思いますので、このデータ取得部分だけを切り出して専用のuseFetcher()というフックにしておきましょう。

useFetcher()というフックに切り出す

const useFetcher = (url) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  useEffect(() => {
    setData(null);
    setError(null);
    fetcher(url)
      .then((item) => setData(item))
      .catch((err) => setError(err));
  }, [url]);

  return { data, error };
};

そして、先のコンポーネントのコードでも、このフックを利用するように置き換えます。

切り出したフックで置き換えたコンポーネント

const NewsDetail = ({ id }) => {
  const { data, error } = useFetcher(`/item/${id}.json`);

  if (error) return <p>Error: {error.message}</p>;
  if (!data) return <p>Loading...</p>;

  return (
    <div>
      <h2>{data.title}</h2>
      <div style={{ display: "flex", gap: 8 }}>
        <span>📝 {data.by}</span>
        <span>🅿️ {data.score}</span>
        <span>💬 {data.descendants}</span>
      </div>
    </div>
  );
};

このように、表示に関する処理以外をざっくり切り出し共通化できるのは、React Hooksの利点といえます。

これでコードの見通しもよくなって万事解決……と言いたいところですが、このuseFetcher()フックにはまだ改良の余地があります。ここまでのコードの表示は次のようになっています。

データ取得にまつわるさまざまな要件

改良の余地としてまず取り上げたいのは、データのキャッシュです。

現在の実装では、このフックを呼び出すコンポーネントが利用されるたび、APIへのリクエストが発行されてしまいます。取得したいデータのURLがまったく同一の場合でもお構いなしです(ここではAPIサーバー側でのキャッシュ指定は考慮しないものとします)。

// 詳細ページを表示するために1リクエスト
useFetcher("/item/1.json");

// 一覧ページへ戻って、もう一度同じ詳細ページを開いたときも1リクエスト
useFetcher("/item/1.json");

// 別のコンポーネントで同じデータを使おうとしても1リクエスト
useFetcher("/item/1.json");

その度にコンポーネントはローディング表示になってしまい、(さっきと同じかもしれない)レスポンスを取得してから表示されるため、ユーザー体験が損なわれてしまいます。そのため、一定期間はキャッシュを保持しておいて、無駄なリクエストを発行せずに即座にページを表示したいというケースは多いです。

また、そのようなキャッシュ機構を導入した場合には、そのキャッシュを任意のタイミングで破棄できる仕組みも必要です。どこか別のところでリソースが更新された場合に、キャッシュを破棄して最新のデータを表示したいからです。

ほかにも、リアルタイム性の高いデータをポーリングして再取得したいという要望もあるかもしれません。

常に最新のデータを表示させるためには、タイマーを用意して一定の間隔でデータを取得するという処理が必要です。その場合も、ブラウザがバックグラウンドにいる間はタイマーを止める、フォアグラウンドで再開するといったチューニングも必要でしょう。

APIからエラーが返ってきた場合は、リトライしたくなることもあるかもしれません。ネットワークがオフラインから復帰した場合にも、自動でリフレッシュさせたいかもしれません。

……というように、データ取得と一口に言っても、実案件ではそれなりの要件が積み重なるもので、その分の実装が必要になることがわかると思います。

そこで活用したいのが、こういったキャッシュ戦略やデータ取得にまつわる便利な機能をまとめて実装しているSWRというライブラリなのです。

SWRを導入する

SWRはnpmからインストールして利用します。

npm i swr

記事執筆時点での最新バージョンは1.0.1で、Reactのバージョンは16.11.0以上が必要です。swr本体の実装は、他のパッケージに依存していない軽量なライブラリとなっています。

先ほどのコンポーネントのコードをswrを使ったものに書き換えると、次のようになります。

useFetcher()フックをSWRを使ったものに書き換える

import useSWR from "swr";

const NewsDetail = ({ id }) => {
  // const { data, error } = useFetcher(`/item/${id}.json`);
  const { data, error } = useSWR(`/item/${id}.json`, fetcher);

  if (error) return <p>Error: {error.message}</p>;
  if (!data) return <p>Loading...</p>;

  return (
    <div>
      <h2>{data.title}</h2>
      <div style={{ display: "flex", gap: 8 }}>
        <span>📝 {data.by}</span>
        <span>🅿️ {data.score}</span>
        <span>💬 {data.descendants}</span>
      </div>
    </div>
  );
};

データ取得部分の処理はすでに専用のフックにしてあったので、利用するコード側に大きな変更はありません。useFetcher()フックを、インストールしたuseSWR()フックに置き換えただけです。

置き換えただけですが、以前のコードと同等に動作するだけでなく、さまざまな機能が追加されています。

内部的な実装も同じ

swrで置き換えても同等に動作すると述べましたが、同じように動くということは内部的な実装も基本的には同じだということです。このことを覚えておくと、useSWR()フックを内部的に利用するカスタムフックを作成するときなどに役立ちます。

useSWR()フック

SWRのライブラリとしての主な機能は、このuseSWR()フックに集約されているといっても過言ではありません。

APIとしての返り値や引数は、次の通りです。

const { data, error, isValidating, mutate } = useSWR(key, fetcher, options);

それぞれ順に見ていきます。

パラメータ

まずはuseSWR()に渡している3つの引数についてです。

  • key
  • fetcher
  • options

keyは、そのデータ取得を一意に識別するためのキーとなります。同じキーに対するuseSWR()フックの呼び出しがグルーピングされ、すでにキャッシュがあればそれを利用できるイメージです。

もっともわかりやすいキーは、URLのパスにあたる情報でしょうか。

useSWR("/api/authors", fetcher);
useSWR("/api/news", fetcher);
useSWR("/api/news/123", fetcher);

このkeyには、文字列以外にも関数や配列やnullを渡すことができます。これらの詳細については次回以降の記事で追って解説します。

次にfetcherで、これは実際にデータを取得する処理それ自体です。ほとんどの場合では、次のようにfetch()をラップした独自の関数になると思います。

const fetcher = (url) =>
  fetch("https://hacker-news.firebaseio.com/v0" + url).then((res) =>
    res.json()
  );

useSWR("/item/29060272.json", fetcher);

このfetcher関数には、引数として先ほどkeyで指定した値が渡されてきます。このコード例の場合、引数url"/item/29060272.json"という文字列になります。

fetcherの実装は完全にユーザーに一任されていて、決まりとしてはPromiseを返す関数であることだけです。よって、fetch()ではなくAxiosなどのライブラリを使うこともできますし、fetch()ではなくローカルのIndexedDBなどからデータを取得するコードでも構いません。

最後にoptionsですが、その名のとおりuseSWR()フックの挙動を調整するオプション群です。

useSWR("/item/29060272.json", fetcher, { shouldRetryOnError: false });

たくさんの設定項目がありますので、詳細はドキュメントを確認してください。デフォルトでもちょうどいい値が指定されているので、利用しなくてもいいかもしれません。

返り値

次に、返り値として得られるオブジェクトに含まれる4つのプロパティです。

  • data
  • error
  • isValidating
  • mutate()

まずdataerrorは、それぞれfetcherの実行結果です。Promiseを返すfetcherが正常に終了した場合(fulfilled)は、得られた値がdataに格納され、エラー終了した場合(rejected)は、エラーがerrorに格納されます。

次にisValidatingmutate()ですが、今回の記事では割愛します。これらの詳細を理解するためには、冒頭で触れていたstale-while-revalidateという戦略について知っておく必要があるため、次回以降の記事で解説します。

今の時点では、このdataerrorの値が更新される度に、useSWR()フックを利用しているコンポーネントが再描画されるという認識で大丈夫です。

おわりに

今回の記事では、Reactにおけるデータ取得の基本についておさらいし、それはSWRで簡単に置き換えられることを解説しました。useSWR()フック自体はとてもシンプルなAPIであり、どんなプロジェクトでも簡単に導入できそうなことがわかったと思います。

次回の記事では、stale-while-revalidateというキャッシュ戦略そのものについて紹介し、その理解を深めていきます。

杉浦 有右嗣
PixelGrid Inc.
シニアエンジニア

SIerとしてシステム開発の上流工程を経験した後、大手インターネット企業でモバイルブラウザ向けソーシャルゲーム開発を数多く経験した。2015年にピクセルグリッドへ入社し、フロントエンド・エンジニアとして数々のWebアプリ制作を手掛ける。2018年に大手通信会社に転職し、低遅延配信の技術やプロトコルを使ったプラットフォームの開発と運用に携わっていたが、2020年ピクセルグリッドに再び入社。プライベートでのOSS公開やコントリビュート経験を活かしながら、実務ではクライアントにとって、ちょうどいいエンジニアリングを日々探求している。

この記事についてのご意見・ご感想 この記事をXにポストする

全記事アクセス+月4回配信、月額880円(税込)

CodeGridを購読する

初めてお申し込みの方には、30日間無料でお使いいただけます