😀

useEffectでfetchするのは良くない?useSWRの使い方

に公開

useEffectでfetchしていませんか?

ReactやNext.jsでAPIを叩くとき、useEffect+fetchを使っていませんか?
もちろんそれでも動きますが、よりシンプルに扱いたいなら、useSWRの導入をおすすめします。
本記事では、
・なぜuseEffectを使ったデータ取得は複雑になるのか
useSWRを使うとどれだけスッキリ書けるのか
useSWRのキャッシュ戦略・再検証の仕組み
について実例を交えて解説します。

この記事の対象者

useSWRを使ったことがない・概要を知りたい
・React初心者〜中級者
・実務でキャッシュの扱いに困っている人

useSWRとは

useSWRはデータ取得のためのReactHooksライブラリで、Vercel社の作ったライブラリです。
“SWR” という名前は、HTTP RFC 5861(opens in a new tab) で提唱されたHTTPキャッシュ無効化戦略である stale-while-revalidate に由来しています。
簡単に言うとuseSWRはまずキャッシュがあれば即座に返し、バックグラウンドでフェッチリクエストを送って再検証(revalidate)を行います。キャッシュがない場合は、即座にフェッチが走ります。
コード例は以下の通りです。

import useSWR from 'swr'

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

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)
 
  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

useSWRの第一引数にはkey文字列(APIのURL)を、第二引数にはfetcher関数を指定します。
fetcher関数は、データを返す任意の非同期関数で、ネイティブのfetchAxiosのようなツールを使うことができます。
data, error, isLoadingの3つの値を返し、それぞれ状態管理をしなくてもそのままページで使うことができます。
特徴は...
・速い、軽量そして再利用可能なデータの取得
・組み込みのキャッシュとリクエストの重複排除
・フォーカス時の再検証
・状態管理なしで loading, error, data を取得
などなど他にもたくさんあります。

まずはuseEffectでfetchした場合

まずは従来のuseEffectを使った書き方から。

"use client";
import { useEffect, useState } from "react";

const Page = () => {
  const [users, setUsers] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const getData = async () => {
      try {
        const res = await fetch(
          "https://687cd72a918b6422433016e9.mockapi.io/api/test/users"
        );
        if (!res.ok) {
          throw new Error("Failed to fetch");
        }
        const data = await res.json();
        setUsers(data);
      } catch (err) {
        setError(err);
      } finally {
        setIsLoading(false);
      }
    };

    getData();
  }, []);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <>
      {users.map((user) => (
        <div key={user.id}>
          <p>ID: {user.id}</p>
          <p>Name: {user.name}</p>
          <p>Created At: {user.createdAt}</p>
        </div>
      ))}
    </>
  );
};

export default Page;

useEffectを使った書き方は...
・useState が複雑(データ・ローディング・エラー管理)
・エラー処理や再フェッチが面倒
・キャッシュ管理が少し面倒

useSWRに置き換え

npm i swrで導入

npm i swr

useSWRを使って上記のコードを書き換えたコードは以下のようになります。

"use client";
import useSWR from "swr";

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

const Page = () => {
  const { data, error, isLoading } = useSWR(
    "https://687cd72a918b6422433016e9.mockapi.io/api/test/users",
    fetcher
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {data.map((user) => (
        <div key={user.id}>
          <p>ID: {user.id}</p>
          <p>name: {user.name}</p>
          <p>createdAt: {user.createdAt}</p>
        </div>
      ))}
    </div>
  );
};

export default Page;

非常にシンプルになりました。
useSWRを使って書くと...
・状態管理不要(useSWRが持ってくれる)
・キャッシュ・自動再フェッチ付き
・コードが圧倒的にシンプル

さらに、共通のAPI呼び出しはカスタムフックにすると再利用しやすくなります。
hooks/useUsers.js

"use client";
import useSWR from "swr";

const fetcher = (url) => {
  return fetch(url).then((res) => res.json());
};

export const useUsers = () => {
  const { data, error, isLoading } = useSWR(
    "https://687cd72a918b6422433016e9.mockapi.io/api/test/users",
    fetcher
  );

  return {
    users: data,
    error,
    isLoading,
  };
};


app/page.jsx

"use client";
import { useUsers } from "@/hooks/useUsers";

const Page = () => {
  const { users, error, isLoading } = useUsers();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>
          <p>ID: {user.id}</p>
          <p>name: {user.name}</p>
          <p>createdAt: {user.createdAt}</p>
        </div>
      ))}
    </div>
  );
};

export default Page;

useSWRのキャッシュ戦略について

useSWRはまずキャッシュがあれば即座に返し、バックグラウンドでフェッチリクエストを送って再検証(revalidate)を行います。キャッシュがない場合は、即座にフェッチが走ります。

またデフォルトでuseSWRは、グローバルキャッシュを使用して、すべてのコンポーネント間でデータを保存および共有します。ただし、以下のようにSWRConfigのproviderオプションを使用して、その挙動をカスタマイズできるそうです。

import { SWRConfig } from 'swr';

<SWRConfig value={{
  refreshInterval: 5000,
  dedupingInterval: 0,
  fetcher: (url) => fetch(url).then(res => res.json()),
}}>
  <App />
</SWRConfig>

また、useSWRは重複排除の機能も備わっており、以下のようなコードの場合、<Avatar>コンポーネントが内部的にuseSWRフックを使用しているとき、一度しかリクエストをしないそうです。

function useUser () {
  return useSWR('/api/user', fetcher)
}
 
function Avatar () {
  const { data, error } = useUser()
 
  if (error) return <Error />
  if (!data) return <Spinner />
 
  return <img src={data.avatar_url} />
}
 
function App () {
  return <>
    <Avatar />
    <Avatar />
    <Avatar />
    <Avatar />
    <Avatar />
  </>
}

再検証のタイミング

再検証のタイミングはいろいろあるそうですが、3つ紹介しておきます。

フォーカス時の再検証

ページにフォーカスを合わせるかタブを切りかえると、SWR は自動的にデータを再検証します。
公式の動画がわかりやすいです。

定期的な再検証

下記のようにすると5秒間隔で再検証してくれるそうです。

useSWR('/api/todos', fetcher, { refreshInterval: 5000 })

しかし実践してみたところ2秒くらいで再検証されました。
調べてみたところ、SWRにはリクエストの重複を防ぐためのdedupingInterval(デフォルト:2000ms = 2秒)という設定があるらしいので、下記のようにしたのですがそれでもうまくいきませんでした。
5秒に設定する方法わかる方いらっしゃいましたら教えていただきたいです。

useSWR('/api/todos', fetcher, { refreshInterval: 5000, dedupingInterval: 0 })

再接続時の再検証

データが常に最新であることを確認するために、SWRはネットワークが回復したときに自動的に再検証してくれるそうです。
ユーザーがオンラインに戻ったときに再検証されるらしいです。

感想

今回はuseSWRに関して概要を調べてみましたが、クライアントコンポーネントでのデータ取得において、useSWRは非常に強力で便利だと実感しました。
今回は概要を説明したかったのでmutate, fallbackData, SWRConfigなど、より実践的なシナリオやチューニング法の説明は割愛させていただきました。

参考
https://qiita.com/musenmai/items/e09c0b798a05522a33cf
https://youtu.be/Kb8YbE8NxZ0

Discussion