🐙

SWRを使おうぜという話

2021/01/12に公開

書き直しました。

https://zenn.dev/mast1ff/articles/5b48a87242f9f0
たくさんの方にリアクション頂き、感謝しております。
当記事掲載当時より、ライブラリのAPIに多数の変更がありましたので、最新のAPIに沿って書き直しました。
これからSWRを検証される際は、上記URLより新しい記事をご参照くださいませ。

本文

vercelといえばNext.jsが有名です。
https://nextjs.org/
Reactを使う人にとって、そして特にフロントエンド専門のエンジニアにとっては悩みの種だったサーバー側との連携の仕方について
「ちょうどよい」規模と多角アプローチで解決してくれる、言わずと知れた小さなフレームワークです。
また、同社が提供するvercelを使用することで、AWSを使用した簡易的なAPIを抽象的に実装できることで、小さなアプリならNext.js一つで完結させることもできるようになっています。

ですが今回はこのNext.jsではなく、私の周りのプログラマでも意外と知らない人が多かった同社の開発する「SWR」について触れたいと思います。
https://swr.vercel.app/

SWRとはなにか

SWRは、クライアント側のデータ取得を、Reactで状態管理しやすいようにしてくれるReact Hooksとそれを内包するライブラリです。

SWRを使わない実装

Next.jsでは、getServerSidePropsやgetStaticPropsなどのサーバーサイドで実行する関数を用意しており、データの取得をサーバー側で行い、Reactコンポーネントをhtmlとして生成した状態でブラウザに渡すことができます。

ですが、getServerSidePropsで複雑な処理をし過ぎると、当然レスポンスまでの時間が長くなり、ユーザー体験を著しく損なってしまいます。
そうなればクライアントサイドでデータフェッチを行うのは、Reactを使用しているならば寧ろ当たり前の処理となります。

例えば記事一覧を取得して→ログインしている状態であれば記事を表示、みたいな機能なら、丁寧に書くなら下記のようになるでしょう。

import type { GetServerSideProps } from 'next';
import Link from 'next/link';
import * as React from 'react';
import { fetchPosts } from '../../_lib/posts';

interface P {
    posts: {
        slug: string
        title: string
	content: string
    }[]
}

const Home: React.FC<P> = ({ posts }) => {
    const [isLoggedIn, setIsLoggedIn] = React.useState<boolean>(false);
    React.useEffect(
        () => {
	    fetch('/api/user')
	        .then(
		    (res) => {
		        const user = res.json();
			setIsLoggedIn(!!user);
		    },
		);
        }, [],
    );
    
    return (
        <div>
	    {isLoggedIn
	        ? (
		    <>
		        {posts.map(
			    (post) => {
			        return (
		         	    <div key={post.slug}>
				        <Link href={`/posts/${post.slug}`}>
				            <a aria-label={post.title}>
					        {post.title}
					    </a>
					</Link>
				    </div>
				);
			    };
			)}
		    </>
		)
		: null}
	</div>
    );
};

export const getServerSideProps: GetServerSideProps = async ({}) => {
    const posts = await fetchPosts();
    return {
        props: {
	    posts,
	},
    };
};

もちろん、axiosやsuperagentなどのfetchを代替するライブラリを使用するケースが多いと思うので、こんなに冗長になることは稀でしょう。
ですが、ここに

  • fetchしたuserがnullの場合とfalseの場合
  • fetchに失敗した場合

などを追加すると・・・

/** 一部抜粋 */
const [isLoggedIn, setIsLoggedIn] = React.useState<boolean | null>(false);
React.useEffect(
    () => {
       fetch('/api/user')
	    .then(
	        (res) => {
		    const user = res.json();
		    /** userはboolean or null とする */
		    if (typeof user !== undefined) {
		        setIsLoggedIn(user);
		    } else {
		        setIsLoggedIn(false);
		    }
		},
	    );
    }, [],
);

などとなります。
例のわかりづらさはともかく、こうしたfetchしたデータの状態管理は、ビューのロジックを複雑にしてしまいます。

SWRの出番

まずは例として、上記の抜粋部分を同じことが出来るように実装してみます。

ipmort type { GetServerSideProps } from 'next';
import Link from 'next/link';
import useSWR from 'swr';
import * as React from 'react';
import { fetchPosts } from '../../_lib/posts';

/** 省略 */

const Home: React.FC<P> = ({ posts }) => {
    /*
    const [isLoggedIn, setIsLoggedIn] = React.useState<boolean>(false);
    React.useEffect(
        () => {
	    fetch('/api/user')
	        .then(
		    (res) => {
		        const user = res.json();
			setIsLoggedIn(!!user);
		    },
		);
        }, [],
    );
    */
    async function fetcher(url: string): Promise<boolean | null> {
        const response = await fetch(url);
	return response.json();
    }
    const { data: isLoggedIn } = useSWR('/api/user', fetcher);
    
    return (
        /** 省略 */
    );
};

これだけ!とても簡単です!
ここでは、importしたuseSWR関数の

  • 第一引数にfetchするURL文字列(この文字列は後述するキーも兼ねます)
  • 第二引数に、第一引数で渡したURLを引数に取りデータを返却するfetch関数
    を渡します。
    返り値のオブジェクトの中のdataがfetch関数で返却したデータとなります。

ここで特筆すべきは、このdataオブジェクトの状態は一貫していて、

  • 取得が完了していない場合、もしくはエラーなどで取得できなかった場合はundefined
  • 取得後はfetch関数の戻り値に明示した型及びデータ
    となります。

ですので、データ取得中は「Loading...」を表示したいなどの場合は、このdataがundefinedの場合の処理を書けばOKです。

SWRの優秀なところ

データ取得における状態管理

上述の通りのため割愛。とにかく状態管理がしやすいです。

データの再フェッチ

SWRはポーリングによるデータの自動再フェッチを行います。
取得するデータが変更された場合に、明示的に関数を再実行することなくデータの更新を行うことが出来ます。
※実行環境によっては再フェッチされない場合もあります。詳細は公式ドキュメントを参照。

データのキャッシュ

SWRで取得したデータは、第一引数で渡した文字列をキーとしてデータをキャッシュします。
キャッシュの中から古いデータを取得し、その後にfetch関数を実行して新しいデータを取得します。
これにより、

  • SPAでのページ遷移後のデータの再取得
  • 再実行時のデータの維持
    がコード一行で完結します。これはすごい!
    これにより不要なデータフェッチが行われないため、小さなコンポーネント単位でも使用することが出来、いわゆる「propsのバケツリレー」やReduxの導入をなくしても外部データの状態管理が実装できます。

気の利いたオプション群

SWRはシンプルで機能こそ限られたライブラリですが、実行時のオプションや関数の戻り値一つ一つにとても気が利いています。
その一例を紹介します。

isValidating

const { data, isValidating } = useSWR('/api/user', fetcher);
/** isValidating: boolean */

データのリクエスト中、または再フェッチの場合にtrueになります。

error

const { data, error } = useSWR('/api/user', fetcher);

errorオブジェクトを取得できます。
errorの場合の表示の切り替えなども、ここで実装できます。

revalidateOnFocus

useSWR('/api/user', fetcher, { revalidateOnFocus: true });

ブラウザのウィンドウがフォーカスされたときに自動で再フェッチします。

revalidateOnReconnect

useSWR('/api/user', fetcher, { revalidateOnReconnect: true });

ネットワーク接続が切れて、回復したときに自動的に再フェッチします。

以上は一例です。
詳細なオプション群は下記ですべて確認できます。
https://swr.vercel.app/docs/options


最後に

Reactでのデータ取得→状態管理の流れは、実装を間違えると複雑になりがちです。
また、アプリが巨大化してくるほど、そのロジックの一部を変更するだけでも一苦労になります。
データを取得するバックエンドやAPIのエンドポイントさえ決まっていれば、どのプロジェクトのどのコンポーネントでも使用できるのがうれしいですね。

自前だと複雑な実装がスッキリとするので、一部だけでも書き換えて試しに使ってみてください。

Discussion