この記事について
SWRを調べて得た知見をざっくりまとめた記事。
キャッシュを管理するライブラリを使用するのは初めてだった為、その目線から感じた疑問や、この系統を調べると必ず出てくる「キャッシュ戦略」についてもやんわりまとめた。
この記事でまとめたこと
- SWRの概要について
- キャッシュ管理系ライブラリのざっくりとした概要・種類
-
stale-while-revalidate
を利用したキャッシュ戦略について
そもそもキャッシュ管理系ライブラリとは?
この系統のライブラリの立ち位置としては状態管理の中に属する部類
状態管理の中でも、キャッシュの状態管理に特化したライブラリ
キャッシュ管理ライブラリの特徴
- データfetchを行ってくれて、fetchしたデータをキャッシュとして管理できる
- キャッシュを保存するだけでは無く、サーバ側とのデータの同期や更新も可能◎
- データ取得、ローディング状態、エラーが発生した時をシンプルに記述できる
- キャッシュ管理のロジックを手書きする必要がなくなる
世の中には様々なキャッシュ管理ライブラリが存在する
フレームワークに合わせて様々なキャッシュ管理ライブラリがある
全フレームワーク向け:RTK Query
React向け:SWR / React Query / Apollo Client
Vue向け:swrv
SWR とは?
Vercel社が開発したライブラリで、データフェッチやキャッシュを管理するためのライブラリ
名前の由来
RFC 5861 で提唱されたキャッシュ戦略である stale-while-revalidate
の略称
※下の方で記述していますがSWRでは全く同じ仕組みではなく、この思想に基づいた設計になってるイメージ
stale-while-revalidate とは?
HTTP ヘッダのCache-Control
に設定できるディレクティブの一つ(MDN)
Cache-Control: max-age=600, stale-while-revalidate=30 // これのこと!
stale-while-revalidate を設定した時の具体的なキャッシュを利用する流れ
キャッシュがない場合
とキャッシュがある場合
でざっくり2パターンある
キャッシュがない場合
- サーバーにフェッチリクエストを投げる
- レスポンスをキャッシュとして保存しクライアントへ即表示
キャッシュがある場合
- 保存されているキャッシュをクライアントに表示させる
- max-ageの期間以上になったら、サーバーにフェッチリクエストを送る
- 差分があれば保存されているキャッシュを更新する
- 何らかのリクエストがあったタイミングでそのキャッシュをクライアント側に表示
SWR と stale-while-revalidate で若干異なるところ
キャッシュがある場合
のキャッシュを表示するタイミングが若干違う
stale-while-revalidate の場合
→ 非同期でキャッシュを更新後、リクエストがあったタイミングで最新のキャッシュを表示する
SWRの場合
→ 非同期でキャッシュを更新後、即反映する
なので、SWRの方がキャッシュの反映が早い!! (サンプル)
SWR ができること
- 高速なページナビゲーション(キャッシュがある場合はそれをすぐ出す)
- 定期的・自動でポーリングする
- データをfetchするフックに関連付けられているコンポーネントが画面に表示されている場合にのみ、再フェッチが行われます。
- フォーカス時・ネットワーク回復時の再検証
- ローカルキャッシュの更新(mutate)
- リクエストの重複排除
- React Nativeでも使用可能◎
SWR の使い方
めちゃめちゃシンプルに書ける!!!
import useSWR from 'swr'
function Profile () {
// 第一引数にキャッシュキー、第二引数にfetcherを渡す。fetcherは事前に用意する必要がある
// 第三引数にはoptionを渡せるが省略も可能
const { data, error } = useSWR('/api/user/123', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
// データをレンダリングする
return <div>hello {data.name}!</div>
}
キャッシュキーとは?
- SWR内部のキャッシュを管理しているkey
- ユニークな文字列を設定する必要がある
fetcherとは?
- fetchする関数がラップされている関数のこと(参考)
- イメージとしては、useSWRの第一引数に入ってくるURLを任意のライブラリでリクエストしてデータを取得してくる感じ。ライブラリはPromiseを返すものであればなんでもOK
const fetcher = (url: string) => fetch(url).then(r => r.json())
axiosを使用する場合はこうなる
import axios from 'axios'
const fetcher = url => axios.get(url).then(res => res.data)
function App () {
const { data, error } = useSWR('/api/data', fetcher)
// ...
}
GraphQLを使用する場合はこうなる
import { request } from 'graphql-request'
const fetcher = query => request('/api/graphql', query)
function App () {
const { data, error } = useSWR(
`{
Movie(title: "Inception") {
releaseDate
actors {
name
}
}
}`,
fetcher
)
// ...
}
SWR の便利・お気に入りなところ
- Reduxで管理する場合と比べて管理しやすくなり最高
- キャッシュの更新が簡単
- パフォーマンスが最適化されている
1. Reduxで管理する場合と比べて管理しやすくなり最高
Reduxを使ってキャッシュ管理した場合
- fetchを実行するsliceを作成する
- fetchを実行した結果をキャッシュとして保存するsliceを作成する
- sliceのカスタムフックを作成する → これをページで使用する
合計:3ファイル作成
SWRでキャッシュ管理した場合
- useSWRを内包したカスタムフックを作成する → これをページで使用する
- fetchを実行+キャッシュを管理
合計:1ファイル作成
ファイル数が一気に減ったので管理しやすい!!!!
2. キャッシュの更新が簡単
POST や DELETE といった処理を行うと現在のローカルで持つキャッシュとサーバーサイドで持つデータの不整合が起きると思います。
この不整合を解消するためにローカルキャッシュの更新を行う必要があり、それを行ってくれるのがMutation
サーバーサイドに強制的にrequestを送り再fetchすることが可能◎
import useSWR, { useSWRConfig } from 'swr'
function App () {
const { mutate } = useSWRConfig()
return (
<div>
<Profile />
<button onClick={() => {
// クッキーを期限切れとして設定します
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
// このキーを使用してすべての SWR に再検証するように指示します
mutate('/api/user')
}}>
Logout
</button>
</div>
)
}
サーバ側の変更を待たずにローカルキャッシュを書き換えることも可能◎
import useSWR, { useSWRConfig } from 'swr'
function Profile () {
const { mutate } = useSWRConfig()
const { data } = useSWR('/api/user', fetcher)
return (
<div>
<h1>My name is {data.name}.</h1>
<button onClick={async () => {
const newName = data.name.toUpperCase()
// 第3引数にfalseを設定すると再検証をせずに直ちにローカルデータを更新します
mutate('/api/user', { ...data, name: newName }, false)
// ソースを更新するために API にリクエストを送信します
await requestUpdateUsername(newName)
// ローカルデータが最新であることを確かめるために再検証(再取得)を起動します
mutate('/api/user')
}}>Uppercase my name!</button>
</div>
)
}
3. パフォーマンスが最適化されている(参考)
最適化ポイントはいくつかあるものの、すごいと感じたのは2点
1. リクエストの重複排除
同じ内容のfetchを行ってるコンポーネントを同じ場所に何個も書いた場合、実際にrequestされるのはその個数分ではなく一回のみ!
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 />
</>
}
2. レンダリングの最適化
SWR はコンポーネントによって使用されている状態のみを更新する
更新は変数ごとに独立しているため、使用する変数を減らせば減らすほどレンダリングを抑えることが可能◎
例1: data と error を使用していて fetch 中にエラーが出て再実行される場合
以下の例だとAppコンポーネントは4回レンダリングされます↓
function App () {
const { data, error } = useSWR('/api', fetcher)
console.log(data, error)
return null
}
console.log(data, error)
undefined undefined // => フェッチの開始
undefined Error // => フェッチの完了、エラーを取得
undefined Error // => フェッチ再試行の開始
Data undefined // => フェッチ再試行の完了、データを取得
// Appコンポーネントは4回レンダリングされる
例2: data のみ使用してfetch 中にエラーが出て再実行される場合
以下の例だとAppコンポーネントは2回のレンダリングで済む👏よしなにやってくれていて最高!
function App () {
const { data } = useSWR('/api', fetcher)
console.log(data)
return null
}
console.log(data)
undefined // => フェッチの開始
Data // => フェッチの完了
// Appコンポーネントがは2回レンダリングされる
// 公式サイトではこれを「魔法が起きている」と表現されていたw
感想・まとめ
- SWRとstale-while-revalidateでキャッシュを出すタイミングが若干違っているのを知った時は困惑した(同じじゃないのかい!という困惑)
- Reduxで管理していたところをまるっとSWRが担ってくれているのは嬉しい!!!!!
- 調べてるとキャッシュの取り扱いはプログラミング上一番難しいと書かれていたり、キャッシュの設計は永遠の課題などと言われていたので、キャッシュ管理系ライブラリの存在は偉大!