React+SWR

SWRはNow.jsを開発しているZEITによる、データ取得ライブラリです。Next.jsだけではなく、React単体でも使用できます。今回は、このライブラリの必要性に触れてみます。

SWRを使う理由

SWRの主な用途はキャッシュとボイラープレートの排除です。従来のReactアプリケーションで外部リソース(API)の取得を行う場合、以下のようなコードを目にします。


const [data, setData] = React.useState([]);

useEffect(() => {
    fetch("https://some-api/users")
    .then(response => response.json())
    .then(data => {
        setData(data);
    });
}, []);

データ取得を行うたび、この定型文(+エラー処理)が出現します。

例えば、階層の異なるコンポーネントでdataを利用したいとします。propsを介したり、Reduxやcontextで解決することはできますが、コードは複雑になります。「利用するコンポーネントで再リクエストすりゃいいじゃん」と思うかもしれませんが、現状のReactには状態のキャッシュというものが存在しないため、再び同コストが必要になります。

SWRはこういった問題点を解決します。

useSWR

使用するのは、useSWRです。これは、データ保存をするフックです。

const { data, error } = useSWR(key, fetcher, options);
  • data – API取得結果
  • error – エラー内容
  • key – ユニークなキーを指定。REST APIならば、エンドポイントURLです。
  • fetcher – リクエストを行う関数。要件はpromiseを返すということだけ。
  • options – オプション

例えば、以下のようなコードになります。


const fetcher = (...args) => fetch(...args).then(res => res.json());

function App() {
    const { data, error } = useSWR("https://your-api/any", fetcher);

    if (error) return <p>Error</p>
    if (!data) return <p>Loading</p>
  
    return (<SomeComponents data={data} />);
}

エラー処理を含めてもコードがシンプルです。また、別のコンポーネントからuseSWRで同じキーでデータ取得を行うと、キャッシュからデータを取り出してくれます。更にリソース元が更新されている場合は、対応するすべてのコンポーネントが再レンダリングされます。コンポーネントツリーは常に最新データと同期しているということです。

ささっと書きましたが、実はこのキャッシュ周辺の仕様は難しいです。無知ながら掻い摘むと、SWRでは同じキーでリクエストを行うと、dedupingInterval(デフォルトは2秒)内では再リクエストをせずキャッシュを使用します。この期間を過ぎた後だと、再リクエストが起こりキャッシュが再検証されます。このインターバル値は、useSWRのオプションから変更が可能です。詳しく知りたい場合は、Stale-While-Revalidateでググりましょう。SWRはこの仕様に沿っています。


これでuseSWRはマスターです。しかしブログ記事としては物足りないため、次にSWRを使った超簡単なデモアプリでも作成してみます。SWRの他のAPIにも触れます。

Demo Application

MTG(マジックザギャザリング)のカード一覧を表示するという簡易なWebアプリを作成します。素晴らしいことに公式がAPIを公開しているので、これを利用させて頂きます。

以降は、Reactの開発環境が整った段階からの解説になります。コードはCodeSandboxにあります。

まずはパッケージをインストールします。


yarn add swr

queries.jsというファイルを作成し、fetcher関数を定義します。

export const fetcher = (...args) => fetch(...args).then(res => res.json())

App.jsxを以下のようにします。

import React from "react";
import { SWRConfig } from 'swr';
import Card from './components/Card';
import Cards from './components/Cards'

// GlobalSetting
const queryConfig = {
  suspense: true,
};

const App = () => {
  const [activeCard, setActiveCard] = React.useState(null);

  return (
    <>
      <h1>MTG Card List</h1>
      <div className="container">
        <SWRConfig value={queryConfig}>
          <React.Suspense fallback={<div>Loading...</div>}>
            {activeCard ? (
              <Card
                activeCard={activeCard}
                setActiveCard={setActiveCard}
              />
            ) : (
              <Cards setActiveCard={setActiveCard} />
            )}
          </React.Suspense>
        </SWRConfig>
      </div>
    </>
  );
};

export default App;

activeCardに値があればCardを表示し、なければCardsを表示しているだけです。実際に入る値は、カードのid値になります。

SWRConfigはグローバルにuseSWRのクエリオプションを設定するものです。要は全体で使う共通オプションです。ラップした下位のコンポーネントでuseSWRを使う際には、その設定が適用されます。ここではsuspense: trueを設定しています。これは、フェッチ時にReact.Suspenseを利用するオプションです。これがない場合、特に処理を記述しなければデータ取得中に画面が真っ白になります。

次はCards.jsxです。ここでuseSWRを使い、カード一覧の取得と表示をします。

import React from "react";
import useSWR from "swr";
import { fetcher } from "../util/queries";
import Button from "./Button";

export default function Cards({ setActiveCard }) {
  const { data, error } = useSWR(
    "https://api.magicthegathering.io/v1/cards?pageSize=20",
    fetcher
  );

  if (error) return <div>error occured: {error.message}</div>;
  // if (!data) return <div>Loadin now</div>;

  return (
    <>
      {data.cards.map(card => (
        <article key={card.id} className="item">
          <h3>{card.name}</h3>
          <p>{card.text}</p>
          <p>
            <small>{card.flavor ? card.flavor : "no flavor"}</small>
          </p>
          <Button
            onClick={() => {
              setActiveCard(card.id);
            }}
          >
            view
          </Button>
        </article>
      ))}
    </>
  );
}

コメントされているif文がありますが、これは前述したReact.Suspenseを有効にしているため、この1行が実行されることはないという意味です。suspenseを無効にした場合は、このようなコードが必要になります。

次は、単一カードを表示するCard.jsxです。

import React from 'react';
import useSWR from 'swr';
import { fetcher } from '../util/queries';

import Button from './Button';

export default function Card({ activeCard, setActiveCard }) {
  const { data, error } = useSWR(
    `https://api.magicthegathering.io/v1/cards/${activeCard}`,
    fetcher,
  );

  if (error) return <div>error occured: {error.message}</div>;

  const { card } = data;
  return (
    <article>
      <h3>{card.name}</h3>
      <figure>
        <img src={card.imageUrl} alt={card.name} />
      </figure>
      <Button onClick={() => setActiveCard(null)}>Back</Button>
    </article>
  );
}

activeCardには選択したカードのidが入っており、useSWRでフェッチするURLに埋め込まれて一意のキーとなります。残りのコードは、取得したデータを使ってカード情報を表示しているだけです。

最後はButton.jsxです。

import React from "react";

export default function Button({ children, onClick }) {
  const handleClick = e => {
    onClick(e);
  };

  return (
    <>
      <button onClick={handleClick}>{children}</button>
    </>
  );
}

ボタンをクリックしたときにactiveCardの値をリセットするだけのコンポーネントです。

実行結果

カードを選択するとフェッチが起こります。初回は時間がかかりますが、一度リクエストすると結果がキャッシュに残り、再びカードを選択するときはキャッシュからデータが取り出されます。

result

この例ではユニークキー(URL)が決まった後に取得先の内容が変更されることはないため意味はありませんが、dedupingIntervalを超えるとデータの再取得があり、キャッシュは再検証されます。2秒内だと検証リクエストはありません。上の画像はわかりやすいように、dedupingIntervalを10秒にしています。検証リクエストが行われていないのが確認できます。なんにせよキャッシュで高速化します。SWRの方針は、古いものでも表示する内容があれば、まずはそれを表示しよう!です。

尚、カード画像自体はあくまで演出というか、ブラウザ自体の画像キャッシュです。画像データそのものがSWRのキャッシュに入っているというわけではありません。

おわりに

SWRはデータ取得における定型文の排除とキャッシュ機能を提供してくれます。いずれReactにキャッシュ機能が搭載されるかもしれませんが、それまでは利用しておきたいライブラリだと思いました。