対象読者
- JavaScriptとWeb開発の基礎に理解がある方
- Reactを用いたJavaScriptアプリケーション開発の経験者
前提環境
筆者の検証環境は以下の通りです。
- macOS Big Sur 11.2.1
- Node.js 15.8.0/npm 7.5.0
- React 17.0.1
- react-scripts 4.0.2
- SWR 0.4.2
React Hooksで通信結果をキャッシュする
アプリケーション開発において、外部システムとのIO処理はパフォーマンスのボトルネックになりがちです。特に、ネットワークを介した通信はその最たるものでしょう。これを解決するための手法の一つとして、キャッシュ機構を利用する方法があります。ブラウザにも、HTTPヘッダー経由でサーバーと協調しながらキャッシュの有効期間を設定する手段が用意されていますね。サーバーやブラウザ側でキャッシュを制御してくれるのもありがたいのですが、もう少し細やかな制御をしたくなる場合もあります。
例えば、シングルページアプリケーションのメモリが有効な間だけキャッシュが継続していて画面遷移を高速に行うことができ、ブラウザを再起動したらキャッシュが消える、といったライフサイクルにしたい場合は、アプリケーション側でキャッシュを制御したいところです。
Reactの場合、簡単な通信の制御とキャッシュであれば、React HooksのuseStateとuseEffectを利用して実装できます。アプリケーションの最上位のコンポーネントで通信結果を保持する方法を見てみましょう(リスト1)。
function App() {
const [user, setUser] = useState(null); // (2)
const [doRefetch, setDoRefetch] = useState(false); // (1)
useEffect(() => {
// レンダリング後にdoRefetchがtrueだったら通信する
if (doRefetch) {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setDoRefetch(false);
setUser(data); // 読み込みが完了したらデータをセットする
});
}
}, [doRefetch]);
if (user === null) {
// データがまだない場合は読み込み中のUIを表示する
return <Loading />;
}
return (
<div>
<Header user={user} />
<Content user={user} />
</div>
);
}
キャッシュを更新したくなったら(1)で定義した関数でsetDoRefetch(true)を実行すればOKです。これで、ブラウザがページをリロードするまでは、(2)に保持したユーザー情報が維持されます。
リスト1は参考実装でしたが、実用する場合は次のような課題を解決する必要がありそうです。
- 通信先のエンドポイントが増えると実装量が大幅に増えるので簡素にしたい
- キャッシュの更新タイミングを適切に制御したい
- 任意のコンポーネント階層で手軽に通信結果を受け取りたい
- 通信エラーをハンドリングしたい
こういった課題を解決できるカスタムフックを自前で実装するのは、なかなか骨が折れそうです。
SWRとは
SWRはデータ取得時の非同期処理の管理とキャッシュ管理を行うための、React Hooks向けライブラリです。
SWRという名前は、RFC 5861で提唱された、HTTPキャッシュを破棄する方針の一つであるstale-while-revalidateに由来しています。stale-while-revalidateの方針でデータ取得を行う場合、まずは既存のキャッシュを返して、そのすぐ後にバックグラウンドで通信を行って、既存のキャッシュと取得したデータに違いがないかを検証します(この処理はrevalidate=再検証と呼ばれています)。キャッシュを利用して可能な限り素早くレスポンスを返すことと、可能な限りキャッシュを新しい状態に保つことを両立した方針と言えるでしょう。
ライブラリとしてのSWRは、このstale-while-revalidateの方針による通信をより簡便に利用するために作られました。インターフェースはuseSWRというカスタムフックのみです。リスト1の通信処理をuseSWRを用いた形に書き直すと、リスト2の通りになります。
import useSWR from 'swr';
const fetcher = (...args) => fetch(...args).then(res => res.json()); // (2)
function App() {
const { data, error } = useSWR('/api/user', fetcher); // (1)
if (data === null) {
// データがまだない場合は読み込み中のUIを表示する
return <Loading />;
}
return (
<div>
<Header user={data} />
<Content user={data} />
</div>
);
}
非同期処理や通信結果の保持に関する処理が(1)にまとまって、見通しがよくなりました。最終的な通信処理には(2)で設定してあるようにfetch関数を使っているので、通信方法自体に魔法があるわけではありません。SWRの役割は、あくまでも非同期処理の状態管理やキャッシュの管理です。
