この記事はセゾン情報システムズ Advent Calendar 2021 5日目の記事です。
Next.jsを使いプロダクト開発をしてる際に一部でクライアントサイドレンダリング(以降CSR)を実現するためにSWR
を選択しました。SWRについて調べ、実際に使ってみて感じたのは圧倒的な使い勝手の良さでした。
本稿ではそんなSWRの基本的な使い方や特徴について取り上げます。
SWR概要
SWRはVercel社
が提供している、データ取得のためのReact Hooksライブラリ
です。 Next.js
と同じチームが開発しています。
SWRについてVercel社は次のように述べています。
“SWR” という名前は、 HTTP RFC 5861 で提唱された HTTP キャッシュ無効化戦略である stale-while-revalidate に由来しています。 SWR は、まずキャッシュからデータを返し(stale)、次にフェッチリクエストを送り(revalidate)、最後に最新のデータを持ってくるという戦略です。
※「データ取得のための React Hooks ライブラリ – SWR より引用
stale-while-revalidate
SWRを使うにあたりstale-while-revalidate
の理解が必要です。
stale-while-revalidateはCache-Control
の拡張の1つでブラウザキャッシュの非同期更新を行います。
それぞれの単語はstale(古い)-while(期間)-revalidate(再検証)を意味しており、端的に言うと「非同期でキャッシュの更新(再検証)を行い、再検証期間中は古いキャッシュを使う」ということです。
キャッシュの意義
ウェブではキャッシュを活用することでサーバーへのリクエストを減らすことができ、リソース取得の高速化やサーバーの負担軽減を実現できます。
これはパフォーマンスの向上に繋がります。
キャッシュの管理はCache-Control, ExpiresやETag, Last-Modifiedが代表的なものとして挙げられます。しかしこれらは設定運用の煩わしさやサーバーへのリクエストが頻繁に発生したりと、キャッシュの効果を十分に発揮するには至りません。
stale-while-revalidateは前述した問題を解消し、キャッシュの活用に貢献します。
stale-while-revalidateの仕組み
Cache-Control: max-age=600, stale-while-revalidate=30
※ Cache-Control例は 「RFC 5861 - HTTP Cache-Control Extensions for Stale Content」 より引用
上記の例の挙動を順を追って分解すると
- max-ageで指定している600秒間は既存のキャッシュを使用
- 600秒経過後、stale-while-revalidateが起動
- stale-while-revalidateで指定している30秒を上限に、裏側で非同期に再検証(リソースの更新の有無をサーバーに問い合わせ更新があればキャッシュの更新)を行う。この期間にリクエストがあれば1のキャッシュを使用する
- 30秒後、stale-while-revalidateは起動を停止。以降は更新されたキャッシュを使用。30秒で再検証が完了しなかった際は次のリクエストで再び再検証を行う
- 1~4を繰り返す
このようにサーバー側でリソースの更新があると、一定期間経過後に自動でキャッシュを更新してくれる仕組みになっています。
stale-while-revalidateの周辺環境
2021年12月現在、stale-while-revalidateが有効なブラウザは限られています。
Chrome, Edge, Firefoxはサポート対象となっていますが、Safariはサポート対象外になっています。詳細は下記を参照ください。
Cache-Control - HTTP | MDN
また、一部のCDNがstale-while-revalidateに対応しています。
例えばCDNサービスのFastly
はCache-Controlにstale-while-revalidateを設定することができます。詳細は下記を参照ください。
失効済みコンテンツの配信 | Fastly ヘルプガイド
SWRについて
SWRはデータ取得をシンプルに実現します。
また、多様な機能を備えており開発者体験の向上にも貢献します。
主な特徴として下記が挙げられます。
- シンプルな記述
- 非同期処理
- 軽量高速でリアクティブ
- 再利用可能なデータの取得
- 重複したリクエストの削除
- 定期的なポーリングとあらゆる再検証
- バックエンドを問わない
- SSR / ISR / SSG対応
- TypeScript対応
- React サスペンス対応
- ページネーションの構築に便利
- etc...
SWRの使い方
SWRの使い方はシンプルです。
yarn add swr
もしくはnpm install swr
でインストールした後に、コンポーネント内にuseSWR
をインポートして使います。
import useSWR from 'swr'
// fetcher関数
const fetcher = (...args) => fetch(...args).then(res => res.json())
function Profile() {
// データの取得
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
※ useSWR基本形は「データ取得のための React Hooks ライブラリ – SWR」 より引用・一部改変
上記useSWRサンプルコードの挙動を確認します。
fetcher関数を定義し、useSWRの第1引数にURL,第2引数にfetcher関数を指定します。
fetcher関数は非同期で第1引数のURLを受け取りデータを返します。
返り値はdataとして渡され、エラーが発生した際はerrorとして渡されます。
最後に返り値で条件分岐し表示UIを出し分けています。
これがSWRの基本形です。さらに再利用可能にします。
import useSWR from 'swr'
const fetcher = (...args) => fetch(...args).then(res => res.json());
export default function useUser (id) {
const { data, error } = useSWR(`/api/user/${id}`, fetcher)
return {
user: data,
isLoading: !error && !data,
isError: error
}
}
※ useSWR再利用化は「はじめに – SWR」 より引用・一部改変
これで各コンポーネントで再利用可能になるとともに、データの更新を継続的かつ自動的に受け取ることができます。
useEffectを使ったパターンとの比較
useEffectを使って同じデータ取得をすると下記になります。
// ページコンポーネント
function Page () {
const [user, setUser] = useState(null)
// データの取得
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data))
}, [])
// グローバルなローディング状態
if (!user) return <div>loading...</div>
return <div>
<Navbar user={user} />
<Content user={user} />
</div>
}
// 子コンポーネント
function Navbar ({ user }) {
return <div>
...
<Avatar user={user} />
</div>
}
function Content ({ user }) {
return <h1>Welcome back, {user.name}</h1>
}
function Avatar ({ user }) {
return <img src={user.avatar} alt={user.name} />
}
※ useEffect使用例は「はじめに – SWR」 より引用・一部改変
useEffectを使った場合、上位のコンポーネントでデータを取得します。
データの使用が上位のコンポーネントだけで完結すれば問題ないですが、上記のようにpropsで子コンポーネントや孫コンポーネントにデータを渡す際はどうでしょうか?
データの依存関係が発生し管理が難しくなります。それは階層が深くなるほど難しくなります。例えばContextを使えばprops drillingは解決できるかもしれませんが、依存関係は存在したままです。
useSWRはこの問題を解決してくれます。
先述した再利用可サンプルを使い、リファクタリングを行うと下記になります。
// ページコンポーネント
function Page () {
return <div>
<Navbar />
<Content />
</div>
}
// 子コンポーネント
function Navbar () {
return <div>
...
<Avatar />
</div>
}
function Content () {
const { user, isLoading } = useUser()
if (isLoading) return <div>loading...</div>
return <h1>Welcome back, {user.name}</h1>
}
function Avatar () {
const { user, isLoading } = useUser()
if (isLoading) return <div>loading...</div>
return <img src={user.avatar} alt={user.name} />
}
※ リファクタリングは「はじめに – SWR」 より引用
propsによる受け渡しがなくなり、各コンポーネントが上位コンポーネントに依存せずに独立してデータを取得しています。これによりprops drillingとデータ依存の問題が解決されました。
加えて記述も各コンポーネントごとにシンプルになり、UI表示にも柔軟に対応でき、管理も簡単になります。これは見た目とロジックの分離にも役立ち、コンポーネント管理を手助けしてくれます。
重複したリクエストの排除と自動再検証
複数のuseSWRに同じURLを設定した場合、リクエストは自動的に重複排除されキャッシュが共有されるのでパフォーマンスの向上に繋がります。
また、フォーカス時や再接続時などの自動再検証が優れています。
具体的には「ページにフォーカスを合わせる」「ブラウザのタブを切りかえる」「ユーザーの使用端末がスリープから復帰する」「オフラインからオンラインに切り替える」といった際にSWRは自動的にデータを再検証・更新します。
まとめ
SWRはシンプルにデータの更新を継続的に行い、自動的に受け取ることができます。それによる恩恵として高速化によるパフォーマンスの向上、UIのリアクティブ化をもたらしてくれます。
また、SWRには先述した以外にも様々な機能が備わっており、それらはオプションとして設定し使うことが可能です。
SWRのユースケースとして考えられるのはSSGと組み合わせるパターンです。パフォーマンスを考えると可能な限りSSGでの実装が理想ですが、例えばログインページのように強整合性が必要な場面もでてきます。そういった際に強整合性が求められれる部分はSWRを使いCSRで実装し、それ以外はSSGで実装と組み合わせることで問題が解決できます。
また、サーバーへの負担やセキュリティのことを考え、サーバーサイドレンダリングを避けることもあるかと思います。SWRを使ったCSR+SSGの組み合わせはそういった際の代替策としても有効ですし、開発の幅も広がるのではないかと考えています。
参考
Stale-While-Revalidate ヘッダによるブラウザキャッシュの非同期更新
Keeping things fresh with stale-while-revalidate
そうです。わたしがReactをシンプルにするSWRです。