SWRはHTTPキャッシュ無効化戦略の夢を見るのか?
React Queryは色々とやっていくうちに、オプションの多さやキャッシュの扱いの難しさが露見していて、使うのに慣れが必要だと感じた。
今回はReact Queryと( たぶん )双璧をなす、swrについての知見を自分なりにまとめてみる。
ちゃんと記事を書きました😤
SWRの目的と思想
先ずは、swrの思想について理解する必要がある。
これを考慮しないまま使ってしまうと、swrが良く無いモノに見えてしまうため、ちゃんと理解する事は大切だと思う🌝 なので、公式サイトを参考に自分なりのまとめを書いていく💪
思想
- HTTPキャッシュ無効化戦略[1]を取り入れるため
- Jamstack
- React思想
思想としては、stale-while-revalidate
を念頭に置いた設計になっていて、React HooksやSuspenceなどに対応している辺り、Reactの思想も反映している様子。
stale-while-revalidate
については、以下のサイトが参考になりました📖
目的
- 非同期処理を簡単に扱えるようにする
- 高速で軽量で再利用可能なデータフェッチ
- リクエストの重複排除
- 非同期処理の複雑なロジックを隠蔽する( 簡単に扱えるようにする )
- リアクティブな動作の実現
swrを使用する目的は、「 非同期処理を簡単に扱えるようにする 」点が大きいと思う。
とてもシンプルに扱えるので、導入しやすい。
「 リアクティブな動作の実現 」については、サービスによっては合う合わないがあると思うので、その辺の知見は後述したいと思います。
今度は基本的な使い方を見ていく👀
基本的な使い方
以下のソースコードは、公式サイトの概要から引用しています。
import useSWR from 'swr'
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>
}
上記のソースコードを見ると、とてもシンプルな構造になっていることが分かるが、シンプル過ぎてよく分らん😇
なので、ちょっとソースコードを改変しながら見て行く🐈
先ずは、以下の部分。
const { data, error } = useSWR('/api/user', fetcher)
「 fetcher
ってなんですの🙄? 」って思うけど、これはfetch関数をラップした単純な関数になっている。例えば、以下のような関数。
const fetcher = (url: string) => fetch(url).then(r => r.json())
これを先ほどのソースコードにぶち込むと、以下のようになる。
const { data, error } = useSWR(
'/api/user',
(url: string) => {
return fetch(url).then(r => r.json())
}
)
fetcherの引数url
には、useSWRの第一引数に渡した文字列が入ってくる。
なので今回の場合、fetcherがリクエストしているのは/api/user
となる。
因みに、fetcherは別にfetch関数を使わなければならないという事は無く、
Promiseを返す関数であれば、なんでも良い。
なので、Promiseに対応させていれば、どのライブラリでも扱う事が可能。
しかし、swr側がfetch関数を凄く押しているので、fetch関数が一番扱いやすい形となっている。
さて、ここまで分かれば、後は簡単。
const { data, error } = useSWR('/api/user', fetcher)
data
は、fetcherが返した値もしくは、undefined
が入っている。
-
data
がundefined
だと -> ロード中 -
data
がundefined
以外だと -> 通信終了
を意味している。そのため、isLoading
なんてフラグは無い🐧
error
もほとんど一緒。
-
error
がundefined
だと -> エラーが発生していない -
error
がundefined
以外だと -> Promiseがrejectされた
となる。なので、ソースコードの下には、それに対応する分岐が書かれている。
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>
}
これで、基本的な使い方は分かった🕷
Tips
fetcherが、GETかつfetch関数であれば、useSWR
の第二引数を省略することが出来る。
const fetcher = (url: string) => fetch(url).then(res => res.json());
const { data } = useSWR('/api/user', fetcher);
// 上記と同じ処理
const { data } = useSWR('/api/user');
知っておくと便利🤿
今度はオプションについて見て行く🛴
オプションについて
先ず、オプションとはuseSWR
関数の第三引数に渡す値の事を指している。
以下のソースコードだと、options
っていう所。
const { data } = useSWR(key, fetcher, options)
次に、オプション一覧とその解説を紹介する。
ここでは見やすさの観点から、型・デフォルト値・効果のみ紹介する。詳細な挙動などについては、凄く長くなってしまう可能性があるため、別途記事として書くかも。。。
※ 公式サイトを参照しています。
suspense
型 | デフォルト値 | 効果 |
---|---|---|
boolean | false | React Suspenceモードを有効にします。 |
initialData
型 | デフォルト値 | 効果 |
---|---|---|
any | undefined | 初期値を設定します |
revalidateOnMount
型 | デフォルト値 | 効果 |
---|---|---|
boolean | ※以下参照 | コンポーネントがマウントした時に自動的に再検証する |
※ デフォルトでは initialData
が設定されてない場合、マウント時に再検証されます。false
だと initialData
を設定していても、再検証されません。
revalidateOnFocus
型 | デフォルト値 | 効果 |
---|---|---|
boolean | true | ウィンドウがフォーカスされたときに自動的に再検証する |
revalidateOnReconnect
型 | デフォルト値 | 効果 |
---|---|---|
boolean | true | ブラウザがネットワーク接続を回復した時に自動的に再検証する |
refreshInterval
型 | デフォルト値 | 効果 |
---|---|---|
number | 0 | ポーリング間隔のミリ秒(デフォルトでは無効) |
refreshWhenHidden
型 | デフォルト値 | 効果 |
---|---|---|
boolean | false | ウィンドウが非表示の時にポーリングする (navigator.onLine で判断) |
※ refreshInterval
が有効になっている場合にのみ動作します。true
に設定する時は、必ず refreshInterval
を設定してください。
refreshWhenOffline
型 | デフォルト値 | 効果 |
---|---|---|
boolean | false | ブラウザがオフラインの時にポーリングする (navigator.onLine で判断) |
refreshWhenOffline
型 | デフォルト値 | 効果 |
---|---|---|
boolean | false | ブラウザがオフラインの時にポーリングする (navigator.onLine で判断) |
shouldRetryOnError
型 | デフォルト値 | 効果 |
---|---|---|
boolean | true | fetcherにエラーが発生したときに再試行する |
dedupingInterval
型 | デフォルト値 | 効果 |
---|---|---|
number | 2000 | この期間内に同じキーでのリクエストの重複を排除します |
focusThrottleInterval
型 | デフォルト値 | 効果 |
---|---|---|
number | 5000 | この期間中に一度だけ再検証する |
loadingTimeout
型 | デフォルト値 | 効果 |
---|---|---|
number | 3000 |
onLoadingSlow イベントをトリガーするためのタイムアウト |
※ 低速ネットワーク(2G、<= 70Kbps)の場合、loadingTimeout
は5秒になります。
errorRetryInterval
型 | デフォルト値 | 効果 |
---|---|---|
number | 5000 | エラーが発生した時の再試行の間隔 |
※ 低速ネットワーク(2G、<= 70Kbps)の場合、errorRetryInterval
は10秒になります。
errorRetryCount
型 | デフォルト値 | 効果 |
---|---|---|
number | ※以下参照 | 最大エラー再試行回数 |
※ デフォルトでは、exponential backoffアルゴリズムを使用してエラーの再試行を処理します
onLoadingSlow
型 | デフォルト値 | 効果 |
---|---|---|
※以下参照 | undefined | リクエストの読み込みに時間がかかりすぎる場合のコールバック関数 |
// SWROptions は、useSWRの第三引数に渡したオプションのオブジェクトです。
type onLoadingSlow = (key: string, config: SWROptions) => void
onSuccess
型 | デフォルト値 | 効果 |
---|---|---|
※以下参照 | undefined | リクエストが正常に終了したときのコールバック関数 |
// SWROptions は、useSWRの第三引数に渡したオプションのオブジェクトです。
type onSuccess = (data: any, key: string, config: SWROptions) => void
onError
型 | デフォルト値 | 効果 |
---|---|---|
※以下参照 | undefined | リクエストがエラーを返したときのコールバック関数 |
// SWROptions は、useSWRの第三引数に渡したオプションのオブジェクトです。
// ※ error は fetcher が reject した値です
type onError = (error: any, key: string, config: SWROptions) => void
onErrorRetry
型 | デフォルト値 | 効果 |
---|---|---|
※以下参照 | undefined | エラー時の再試行をするコールバック |
// SWROptions は、useSWRの第三引数に渡したオプションのオブジェクトです。
// ※ error は fetcher が reject した値です
type onErrorRetry = (
error : any,
key: string,
config: SWROptions,
revalidate: (options: RevalidateOptions) => Promise<boolean>,
revalidateOptions: RevalidateOptions
) => void
// 再検証のオプション型
type RevalidateOptions = {
dedupe: boolean;
retryCount: number;
}
compare
型 | デフォルト値 | 効果 |
---|---|---|
※以下参照 | ※以下参照 | 誤った再レンダリングを回避するために、返されたデータがいつ変更されたかを検出するために使用される比較関数 |
type compare = (a: any, b: any) => boolean
※ デフォルト値では、dequalが使われています
isPaused
型 | デフォルト値 | 効果 |
---|---|---|
※以下参照 | () => false | 再検証を一時停止するかどうかを検出する関数 |
type isPaused = () => boolean
※ isPaused
が true
を返す時、再検証を停止し、フェッチされたデータとエラーを無視します。デフォルトでは、false
を返します。
詳細は以下のプルリクを参照してください🏳🌈
意外とオプションの数が多かった😥
でも、比較的シンプルな設計になっているように感じた(小並感🎈)
今度はMutationについて見て行こう🧟♀️🧟♀️
Mutationについて
まずMutationとは何かという事を簡単に説明したい。
ここで言うMutationとは、他の処理の影響を受けることだと思って頂ければいい。
分かりやすいように具体例を挙げよう👲
先ず、以下のようなネコ情報を取得する処理があるとする
interface Cat {
name: string;
}
const DisplayCatName: React.FC = () => {
const fetcher = url => fetch(url).then(res => res.json() as Cat)
const { data: cat, error } = useSWR<Cat>("/cats", fetcher);
if( error ) return <p>エラーが発生しました😿</p>
return <p> ネコの名前は {cat.name} です😸</p>;
}
上記のソースコードは現状では問題ないが、これにネコ情報を更新する処理を追加すると問題が発生する。
interface Cat {
name: string;
}
const onUpdateCatName = () => {
/* -- ネコの名前を更新する処理 🐈 -- */
}
const DisplayCatName: React.FC = () => {
const fetcher = url => fetch(url).then(res => res.json() as Cat)
const { data: cat, error } = useSWR<Cat>("/cats", fetcher);
if( error ) return <p>エラーが発生しました😿</p>
return (
<>
<p> ネコの名前は {cat.name} です😸</p>;
{/* ネコ情報を更新するフォーム */}
<form onSubmit={onUpdateCatName} >
<input name="cat_name" defaultValue={cat.name} />
<button type="submit">猫の名前を更新する</button>
</form>
</>
);
}
上記のソースコードで、フォームにネコの名前を入力し送信ボタンを押すとonUpdateCatName
が実行されてネコの名前が更新されるが、この時、既に表示されているネコの名前にはその更新が反映されない。
これを反映させるには、useSWR
に変更したことを伝える必要がある。
そして、この useSWR
に変更したことを伝える機能こそがMutationと言える。
以上、簡単解説終わり🍃
SWRでMutationを扱う方法
先述した通り、データの更新などの処理がある場合 useSWR
に変更したことを伝える必要がある。
その方法を紹介したいと思うが、とっても簡単なので安心して欲しい🦸♂️
具体的には以下のようにする
方法その1
const DisplayCatName = () => {
const { data: cat, mutate } = useSWR("/cat", fetcher);
const onUpdateCatName = async (catName: string) => {
await postCatName(catName); // ネコの名前を更新する非同期関数
mutate({ ...cat, name: catName }); // ここでSWRに変更を通知する
};
/* -- 省略 -- */
}
useSWR
が返すオブジェクトの中に、mutate
と言う関数がある。これに更新した内容を反映させたデータを渡すと、キャッシュを更新して描画更新をしてくれる。これによって、useSWR
が返した値を使って描画している所は、最新の描画内容となる。
そしてさらに、この方法のほかに三つの方法がある😇
方法その2
先ず一つは、import文からインポートして使うやり方。具体例を見てみよう🏇
import useSWR, { mutate } from "swr";
const DisplayCatName = () => {
const { data: cat } = useSWR("/cat", fetcher);
const onUpdateCatName = async (catName: string) => {
await postCatName(catName); // ネコの名前を更新する非同期関数
// ここでSWRに変更を通知する
// 第三引数にfalseを渡すことで、再検証( 再取得 )する事を防ぐことが出来る
mutate("/cat", { ...cat, name: catName }, false);
// 因みにPromiseが更新内容を返すなら、そのPromiseをそのまま渡すこともできる
// 仮に postCatName の返り値が Promise<Cat> なら以下のように渡すことが出来る
// mutate("/cat", postCatName(catName));
};
/* -- 省略 -- */
}
最初のやり方と違う所は、mutate関数
をインポートから引っ張って来ている事と、実行時にキー文字列( 今回は "/cat" )を渡す必要があるという事。
このやり方が便利な所は、別の場所で使われている useSWR
に変更を通知することが出来る所。これによって、離れた位置のコンポーネントの状態などを変更することが出来るので、うまく使えばキャッシュをより有効活用することが出来る💪
ただ注意としては、使いすぎると複雑なバグを引き起こしかねないので、useSWR
が返す mutate
が使える場合は、そちらを使った方が無難だと思う🤔
あと、関数の名前が一緒なので、名前の競合が起きやすいのでそこにも注意しよう👩🏫
方法その3
次に、もう一つのやり方を見て行こう🪀
import useSWR, { mutate } from "swr";
const DisplayCatName = () => {
const { data: cat } = useSWR("/cat", fetcher);
const onUpdateCatName = async (catName: string) => {
await postCatName(catName); // ネコの名前を更新する非同期関数
mutate("/cat"); // ここでSWRに変更を通知するが、文字列だけ渡している事に注意!
};
/* -- 省略 -- */
}
今度のやり方は、mutate関数
をインポートして使う所は前回と一緒だが、実行する時にキー文字列( 今回は "/cat" )だけを渡している点に注意しよう!
このように実行する事によって、useSWR
にrevalidate( refetch )、つまり第二引数に渡した関数を実行するように指示することが出来る。これによって、通信処理が走ってサーバーがちゃんと更新した内容を返すなら、画面内容は最新のものになるし、キャッシュの有効性や整合性を確認することが出来る。
この方法は、一見すると回りくどいやり方のように見えるが、サーバーで複雑な処理をしている時は、この方法を使わないとデータの整合性が取れないと思うので何気に便利な機能となっている。
方法その4
次に最後のやり方を見て行こう🎯 ( と言ってもほとんど同じだけど。。。)
import useSWR, { mutate } from "swr";
const DisplayCatName = () => {
const onUpdateCatName = async (catName: string) => {
mutate("/cat", async (cat: Cat) => {
const newCat = { ...cat, name: catName };
await postCat(newCat); // ネコの情報を更新する非同期関数
return newCat; // ここで新しい値を返す必要がある!
});
};
/* -- 省略 -- */
}
上記の方法では、インポートしてきたmutate関数
に非同期関数を渡している。
この非同期関数は、キャッシュされている値を受け取って処理をして、新しい値を返す必要がある。
この方法は、現在の取得しているデータを用いた変更処理を行う時に大変便利な方法となっている。これによって、わざわざuseSWR
を実行して値を取得する必要は無いし、複雑な非同期処理も書きやすくすることが出来る。
ただ、こちらも使い方によっては複雑なソースコードになってしまうため、使い方には注意が必要だと思う🔫
まとめ
ここまででMutationのやり方を4つ紹介し知見を語ってきたので、この4つの方法それぞれの メリット ・ デメリット ・優先度をまとめてみる📐
方法名 | メリット | デメリット | 使用する優先度 |
---|---|---|---|
その1 | 簡単にデータを更新できる | 複雑な処理は出来ない, useSWR必須 | 高 |
その2 | useSWRを使わなくてもキャッシュを更新できる | その1で代用できることが多い | 低 |
その3 | データの整合性を高めることが出来る | 非同期処理が走るため処理が遅くなるかも | 中 |
その4 | 複雑な処理が可能 | バグを引き起こしやすい | 中 |
私の見解としては、なるべくは その1 の方法を使って、複雑な処理が必要な場合は その4 を使うという感じ。その 2, 3 の方法は使えるなら使う程度かな。
そういえば、Mutationっていう用語は一般的なのか?
何か調べても、GraphQL関連しかヒットしないが、どうなんだろ🙄?
今度は細かい機能について見て行く🔮
fetcherを条件分岐で実行したい
これは簡単、useSWR
の第一引数に null
( falsyな値 ) を渡すとfetcherを実行しない。
これによって、任意のタイミングで fetcher を実行する事が出来る。
// 条件分岐でフェッチ
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
// falsyな値を返しても同じように出来る
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
// 第一引数の関数内でエラーを発生させても挙動としては同じようになる
// この時のエラーは、返り値の error で取得できないので注意!
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)
ソースコード内にも書いてあるが、一番下のエラーを発生させるやり方は useSWR
がエラーを検出してくれないので、使うべきでは無い。
また、第一引数はなるべくは関数以外の値にすべきだ
関数はどうしても副作用を含んでしまう可能性があるため、なるべくは文字列などのプリミティブ型を使う事をお勧めする。
あと、falsyな値は非同期処理を停止しまう事にも注意しよう👨🚀
以下のサイトからどの値がfalsyな値か確認できるが、JavaScript( TypeScript )はfalsyな値が結構多い。
なので、知らず知らずのうちにfalsyな値を返してしまう事があるので、本当に注意して欲しい📍
複数の値をキーとして渡したい
実は useSWR
の第一引数には配列を渡すことが出来る。
const params: number = useParam(); // url からパラメーターを取得する
// fetcherの引数に配列の値が入ってくる
const { data } = useSWR(["/hoge", params], (url: string, _params: number) => fetcher(url, _params));
この機能は、パラメーターを含むようなフェッチなどを実行する時に使う必要がある。
なぜなら、パラメーターの値によって取得する値が変化するため、それを useSWR
に知らせる必要がある。もし第一引数に文字列のみ渡してしまうと、useSWR
がパラメーターの変化を検知できずにキャッシュを返して、データの整合性が保てなくなってしまう😱
const params: number = useParam(); // url からパラメーターを取得する
// 第一引数の値が変化しない為、paramsが変更してもfetcherが実行されない!
const { data } = useSWR("/hoge", (url: string) => fetcher(url, params));
また useSWR
は、この配列を 浅く比較して 再検証( 再フェッチ )するかどうかを決めるので、深い比較が必要な値はなるべく指定しないようするか、ディープコピーなどして対処しよう🎿
プリフェッチする
以下のような感じで、プリフェッチできる。
import { mutate } from 'swr'
function prefetch () {
mutate('/api/data', fetch('/api/data').then(res => res.json()))
// the second parameter is a Promise
// SWR will use the result when it resolves
}
しかし、一番いいのはTop-Levelでやる事らしいので、こちらが使えるならこちらを使おう🪂
公式サイトでは、こちらの方法を強く勧めている。
以下は、公式サイトからの引用&要約。
SWRのデータをプリフェッチする方法はたくさんあります。トップレベルのリクエストに
rel="preload"
を指定する方法は、強くお勧めします。
<link rel="preload" href="/api/data" as="fetch" crossorigin="anonymous">
HTML内に配置するだけ
<head>
です。簡単、高速、ネイティブです。JavaScriptがダウンロードを開始する前であっても、HTMLがロードされるときにデータをプリフェッチします。 同じURLを使用するすべての着信フェッチ要求は、結果を再利用します(もちろん、SWRを含む)。
今度はページネーションについて見て行く📑
ページネーションについて
一番簡単な方法は、useSWR
のキーを変更する方法。
function App () {
const [pageIndex, setPageIndex] = useState(0);
// The API URL includes the page index, which is a React state.
const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher);
// ... handle loading and error states
return <div>
{data.map(item => <div key={item.id}>{item.name}</div>)}
<button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
<button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
</div>
}
しかし上記の方法では、ページ全体のデータを扱うことが出来ないのでちょっと不便😕
そこで、useSWRInfinite
というHooksが用意されている。
// A function to get the SWR key of each page,
// its return value will be accepted by `fetcher`.
// If `null` is returned, the request of that page won't start.
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null // reached the end
return `/users?page=${pageIndex}&limit=10` // SWR key
}
function App () {
const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
if (!data) return 'loading'
// We can now calculate the number of all users
let totalUsers = 0
for (let i = 0; i < data.length; i++) {
totalUsers += data[i].length
}
return <div>
<p>{totalUsers} users listed</p>
{data.map((users, index) => {
// `data` is an array of each page's API response.
return users.map(user => <div key={user.id}>{user.name}</div>)
})}
<button onClick={() => setSize(size + 1)}>Load More</button>
</div>
}
上記の方法ではページ全体の値を扱うことが出来るうえに、シンプルに実装することが出来るので、ページネーションするなら取り敢えず useSWRInfinite
を使った方が良さそう🐿
今度はAuto Revalidationについて見て行く👩⚖️👩⚖️
Auto Revalidationについて
このAuto Revalidation( 自動再検証 )は、解説するのが難しいので出来れば公式サイトの動画を見た方が良いと思う。
動画を見たら分かると思うが、一言で言ってしまえば「 データを常に最新に保つ 」機能だ。
データを最新に保つためには、その都度データを取得( と再検証 )する必要があるが、SWRでは以下のタイミングで再取得する。
- 画面がフォーカスされた時( Revalidate on Focus )
- 指定された時間ごとに( Revalidate on Interval )
- ネットが再接続された時( Revalidate on Reconnect )
- コンポーネントがマウントした時( Revalidate on Mount )
それぞれ対応したオプションがあるので、必要に応じて切り替えることが出来る🧞♀️
ただ私の見解としては、基本的にRevalidate on Mount以外は全部無効にしておいていいと思う。
なぜなら、そこまで最新に保つ必要が無ければサーバーに負荷を懸けてしまうし、内部で深い比較をしているので、場合によっては負荷が高くなってしまう🤒
ここら辺はサービスによって対応が違うと思うので、自分のサービスと相談しよう🍜
SWRがパフォーマンスについての見解を書いてくれているので、紹介する🍹
パフォーマンス
公式サイトのパフォーマンスのページにその事が書いてあったので、引用&要約する。
SWRは、あらゆる種類のWebアプリで重要な機能を提供するため、パフォーマンスが最優先事項です。
SWRの組み込みのキャッシュと重複排除は、不要なネットワークリクエストをスキップしますが、
useSWR
フック自体のパフォーマンスは依然として重要です。複雑なアプリでuseSWR
は、1ページのレンダリングで数百回の呼び出しが発生する可能性があります。SWRはアプリにコードの変更が無ければ次のものがあることを保証します。
- 不要なリクエストが無い
- 不要な再レンダリングがない
- 不要なコードがインポートされてない
注目して欲しいのは、
「 複雑なアプリでuseSWR
は、1ページのレンダリングで数百回の呼び出しが発生する可能性があります 」
という所。これはAuto Revalidationなどのオプションをちゃんと設定して無いと起こってしまう可能性があるので、ちゃんと注意しておこう📣 ( まぁ、そこまで大量のリクエストが発生してしまうのは別の要因な気がしますが🤔 )
これは説明しなくても大丈夫だと思うが、一応紹介🥢
Deduplication( 重複排除 )
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 />
</>
}
上記のソースコードでは '/api/user'
をリクエストしているが、useSWR
が重複を検知して本来は5回リクエストされるところを一回のリクエストに抑えてくれる。
キャッシュの有効期限は、dedeupingIntervalオプションで指定できるので、キャッシュが効きすぎていると思ったら、適切な値に設定しよう⛩
これは結構混乱する人多いと思うので、内部の挙動を示しながら解説していく☕
Dependency Collection
まず、公式サイトより引用&要約。
useSWR
が返す3つのステートフルな値:data
,error
,isValidating
, それぞれが独立して更新することが出来ます。例えば、完全なデータフェッチライフサイクル内でこれらの値を出力すると、次のようになります:data,error,isValidatingを取得してconsole.logで表示function App () { const { data, error, isValidating } = useSWR('/api', fetcher) console.log(data, error, isValidating) return null }
最悪の場合(最初の要求が失敗し、次に再試行が成功した)、5行のログが表示されます。
console.logの結果// console.log(data, error, isValidating) undefined undefined false // => hydration / initial render undefined undefined true // => start fetching undefined Error false // => end fetching, got an error undefined Error true // => start retrying Data undefined false // => end retrying, get the data
状態変化は理にかなっています。しかし、それはまた、コンポーネントが5回レンダリングされることを意味しています。
コンポーネントを変更して次のものだけを使用する場合
data
:dataだけ取得するように修正(fetcherの挙動は変化してない)function App () { const { data } = useSWR('/api', fetcher) console.log(data) return null }
魔法が起こります—現在、再レンダリングは2つだけです。
console.logの結果// console.log(data) undefined // => hydration / initial render Data // => end retrying, get the data
まったく同じプロセスが内部で発生し、最初のリクエストからエラーが発生し、再試行からデータを取得しました。ただし、SWRはコンポーネントによって使用されている状態のみを更新します。 今回の例の場合は、
data
のみ。
上記の内容では、useSWR
から受け取る値を変更するだけで、コンポーネントの描画更新が変化する。
なぜこのような事が起こるのかと言うと、SWR側が値を取得しているかを検知して、最適な描画更新をしているためである。具体的には、以下のようなソースコード。
※ 公式リポジトリから引用
Object.defineProperties(state, {
error: {
// `key` might be changed in the upcoming hook re-render,
// but the previous state will stay
// so we need to match the latest key and data (fallback to `initialData`)
get: function () {
stateDependencies.current.error = true;
return keyRef.current === key ? stateRef.current.error : initialError;
},
enumerable: true
},
data: {
get: function () {
stateDependencies.current.data = true;
return keyRef.current === key ? stateRef.current.data : initialData;
},
enumerable: true
},
isValidating: {
get: function () {
stateDependencies.current.isValidating = true;
return key ? stateRef.current.isValidating : false;
},
enumerable: true
}
});
上記のソースコードは、getter を利用して値が使われているかを検知して、それぞれを ステートフルな値 として扱っている。
仕組みは意外と簡単なのだが、初見時にちょっと驚いたのは内緒🤫
この挙動を知らずに、使ってもないのに error
や isValidating
を取得してると、無駄な描画更新が発生してしまうので注意しよう🀄
キャッシュについても解説する🚀
キャッシュについて
実はSWRのキャッシュは、インポートして使用することが出来る。
import { cache } from "swr";
type keyType = string | any[] | null;
type keyFunction = () => keyType;
type keyInterface = keyFunction | keyType;
// キャッシュをクリア
cache.clear();
// キャッシュを削除
cache.delete(key: keyInterface) // => void;
// キャッシュを取得
cache.get(key: keyInterface); // => any;
// キャッシュが存在しているか
cache.has(key: keyInterface); // => boolean;
// キャッシュのキー配列を取得
cache.keys(); // => string[];
// シリアライズキーを取得
cache.serializeKey(key: keyInterface); // => [string, any, string, string]
// キャッシュをセットします
cache.set(key: keyInterface, value: any); // => any
// キャッシュの変更を監視します
cache.subscribe(listener: () => void); // => () => void
テスト以外でキャッシュを削除したい場合
テスト以外の環境でcacheを削除したい場合は、 🧜♂️ Mutationを使おう 🧜♀️
大体の機能は紹介できたと思うので、クローズ✅
これを元に記事に清書しようと思います🖊
何かあればコメントしてください🙏