Closed16

SWRはHTTPキャッシュ無効化戦略の夢を見るのか?

ピン留めされたアイテム
uttkuttk

React Queryは色々とやっていくうちに、オプションの多さやキャッシュの扱いの難しさが露見していて、使うのに慣れが必要だと感じた。

今回はReact Queryと( たぶん )双璧をなす、swrについての知見を自分なりにまとめてみる。

uttkuttk

SWRの目的と思想

先ずは、swrの思想について理解する必要がある。
これを考慮しないまま使ってしまうと、swrが良く無いモノに見えてしまうため、ちゃんと理解する事は大切だと思う🌝 なので、公式サイトを参考に自分なりのまとめを書いていく💪

思想

  • HTTPキャッシュ無効化戦略[1]を取り入れるため
  • Jamstack
  • React思想

思想としては、stale-while-revalidateを念頭に置いた設計になっていて、React HooksやSuspenceなどに対応している辺り、Reactの思想も反映している様子。

stale-while-revalidateについては、以下のサイトが参考になりました📖

https://blog.jxck.io/entries/2016-04-16/stale-while-revalidate.html

目的

  • 非同期処理を簡単に扱えるようにする
  • 高速で軽量で再利用可能なデータフェッチ
  • リクエストの重複排除
  • 非同期処理の複雑なロジックを隠蔽する( 簡単に扱えるようにする )
  • リアクティブな動作の実現

swrを使用する目的は、「 非同期処理を簡単に扱えるようにする 」点が大きいと思う。
とてもシンプルに扱えるので、導入しやすい。

「 リアクティブな動作の実現 」については、サービスによっては合う合わないがあると思うので、その辺の知見は後述したいと思います。

脚注
  1. stale-while-revalidateについては、RFCを参照 ↩︎

uttkuttk

今度は基本的な使い方を見ていく👀

基本的な使い方

以下のソースコードは、公式サイトの概要から引用しています。

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が入っている。

  • dataundefined だと -> ロード中
  • dataundefined 以外だと -> 通信終了

を意味している。そのため、isLoadingなんてフラグは無い🐧

errorもほとんど一緒。

  • errorundefined だと -> エラーが発生していない
  • errorundefined 以外だと -> 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');

知っておくと便利🤿

uttkuttk

今度はオプションについて見て行く🛴

オプションについて

先ず、オプションとは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 リクエストの読み込みに時間がかかりすぎる場合のコールバック関数
onLoadingSlowの型
// SWROptions は、useSWRの第三引数に渡したオプションのオブジェクトです。
type onLoadingSlow = (key: string, config: SWROptions) => void

onSuccess

デフォルト値 効果
※以下参照 undefined リクエストが正常に終了したときのコールバック関数
onSuccessの型
// SWROptions は、useSWRの第三引数に渡したオプションのオブジェクトです。
type onSuccess = (data: any, key: string, config: SWROptions) => void

onError

デフォルト値 効果
※以下参照 undefined リクエストがエラーを返したときのコールバック関数
onErrorの型
// SWROptions は、useSWRの第三引数に渡したオプションのオブジェクトです。
// ※ error は fetcher が reject した値です
type onError = (error: any, key: string, config: SWROptions) => void

onErrorRetry

デフォルト値 効果
※以下参照 undefined エラー時の再試行をするコールバック
onErrorRetryの型
// 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

デフォルト値 効果
※以下参照 ※以下参照 誤った再レンダリングを回避するために、返されたデータがいつ変更されたかを検出するために使用される比較関数
compareの型
type compare = (a: any, b: any) => boolean

※ デフォルト値では、dequalが使われています

isPaused

デフォルト値 効果
※以下参照 () => false 再検証を一時停止するかどうかを検出する関数
isPausedの型
type isPaused = () => boolean

isPausedtrue を返す時、再検証を停止し、フェッチされたデータとエラーを無視します。デフォルトでは、false を返します。

詳細は以下のプルリクを参照してください🏳‍🌈

https://github.com/vercel/swr/pull/845

uttkuttk

意外とオプションの数が多かった😥
でも、比較的シンプルな設計になっているように感じた(小並感🎈)

uttkuttk

今度は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

mutateをuseSWRから取得するやり方
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文からインポートして使うやり方。具体例を見てみよう🏇

mutateをインポートして使うやり方
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

次に、もう一つのやり方を見て行こう🪀

refetchするやり方
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 の方法は使えるなら使う程度かな。

uttkuttk

そういえば、Mutationっていう用語は一般的なのか?
何か調べても、GraphQL関連しかヒットしないが、どうなんだろ🙄?

uttkuttk

今度は細かい機能について見て行く🔮

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な値を返してしまう事があるので、本当に注意して欲しい📍

https://developer.mozilla.org/ja/docs/Glossary/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を含む)。

uttkuttk

今度はページネーションについて見て行く📑

ページネーションについて

一番簡単な方法は、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 を使った方が良さそう🐿

uttkuttk

今度はAuto Revalidationについて見て行く👩‍⚖️👩‍⚖️

Auto Revalidationについて

このAuto Revalidation( 自動再検証 )は、解説するのが難しいので出来れば公式サイトの動画を見た方が良いと思う。

https://swr.vercel.app/docs/revalidation

動画を見たら分かると思うが、一言で言ってしまえば「 データを常に最新に保つ 」機能だ。
データを最新に保つためには、その都度データを取得( と再検証 )する必要があるが、SWRでは以下のタイミングで再取得する。

  • 画面がフォーカスされた時( Revalidate on Focus )
  • 指定された時間ごとに( Revalidate on Interval )
  • ネットが再接続された時( Revalidate on Reconnect )
  • コンポーネントがマウントした時( Revalidate on Mount )

それぞれ対応したオプションがあるので、必要に応じて切り替えることが出来る🧞‍♀️

ただ私の見解としては、基本的にRevalidate on Mount以外は全部無効にしておいていいと思う。
なぜなら、そこまで最新に保つ必要が無ければサーバーに負荷を懸けてしまうし、内部で深い比較をしているので、場合によっては負荷が高くなってしまう🤒

ここら辺はサービスによって対応が違うと思うので、自分のサービスと相談しよう🍜

uttkuttk

SWRがパフォーマンスについての見解を書いてくれているので、紹介する🍹

パフォーマンス

公式サイトのパフォーマンスのページにその事が書いてあったので、引用&要約する。

SWRは、あらゆる種類のWebアプリで重要な機能を提供するため、パフォーマンスが最優先事項です。

SWRの組み込みのキャッシュと重複排除は、不要なネットワークリクエストをスキップしますが、useSWR フック自体のパフォーマンスは依然として重要です。複雑なアプリでuseSWRは、1ページのレンダリングで数百回の呼び出しが発生する可能性があります。

SWRはアプリにコードの変更が無ければ次のものがあることを保証します。

  • 不要なリクエストが無い
  • 不要な再レンダリングがない
  • 不要なコードがインポートされてない

注目して欲しいのは、

複雑なアプリでuseSWRは、1ページのレンダリングで数百回の呼び出しが発生する可能性があります

という所。これはAuto Revalidationなどのオプションをちゃんと設定して無いと起こってしまう可能性があるので、ちゃんと注意しておこう📣 ( まぁ、そこまで大量のリクエストが発生してしまうのは別の要因な気がしますが🤔 )

uttkuttk

これは説明しなくても大丈夫だと思うが、一応紹介🥢

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オプションで指定できるので、キャッシュが効きすぎていると思ったら、適切な値に設定しよう⛩

uttkuttk

これは結構混乱する人多いと思うので、内部の挙動を示しながら解説していく☕

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側が値を取得しているかを検知して、最適な描画更新をしているためである。具体的には、以下のようなソースコード。

公式リポジトリから引用

srwのソースコードから抜粋
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 を利用して値が使われているかを検知して、それぞれを ステートフルな値 として扱っている。

仕組みは意外と簡単なのだが、初見時にちょっと驚いたのは内緒🤫

この挙動を知らずに、使ってもないのに errorisValidating を取得してると、無駄な描画更新が発生してしまうので注意しよう🀄

uttkuttk

キャッシュについても解説する🚀

キャッシュについて

実は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を使おう 🧜‍♀️

uttkuttk

大体の機能は紹介できたと思うので、クローズ✅
これを元に記事に清書しようと思います🖊
何かあればコメントしてください🙏

このスクラップは2021/01/23にクローズされました