Open18

useSWRのドキュメントを全部読む

omorimoriomorimori

名前の由来である stale-while-revalidate とは?

stale(陳腐化)
while(有効期間中)
revalidate(再検証)

HTTPヘッダのCache-Controlに設定できるディレクティブ(指示)の一つで、レスポンスヘッダーに設定される。(参考)
キャッシュを再検証している間、古いレスポンスの再利用が可能なことを示す。

Cache-Control: max-age=604800, stale-while-revalidate=86400

この例だと、レスポンスは 7 日間(604800 秒間)は新鮮。7 日後、レスポンスは古くなるが、キャッシュは翌日(86400 秒後)のリクエストに再利用可能。ただし、バックグラウンドでレスポンスを再検証することが条件。

再検証により、キャッシュは再び新鮮になる。クライアントはその期間中は常に新鮮であったかのように見えるので、再検証の遅延ペナルティを効果的にクライアントから隠蔽できる。

用語

  • レスポンスの再利用
    • キャッシュされたレスポンスを次のリクエストに再利用すること。
  • レスポンスの再検証
    • オリジンサーバーに、保存されているレスポンスがまだ新しいか確認すること。通常は条件付きのリクエストで行う。
  • 新鮮なレスポンス
    • レスポンスが新しい状態であることを示す。これは通常、リクエストの指示に応じて、レスポンスを後続のリクエストに再利用できることを意味する。
  • 古いレスポンス
    • レスポンスが古い状態になっていることを示す。これは通常、レスポンスがそのままでは再利用できないことを意味する。キャッシュストレージは古いレスポンスをすぐに削除する必要はない。なぜなら、再検証によってレスポンスが古いものから再び新しい状態に変わる可能性があるためである。

more

これを理解するためにはキャッシュの全体像を知っていたほうがよい

omorimoriomorimori

SWRは状態機械として理解することができる

https://swr.vercel.app/ja/docs/advanced/understanding

useSWR は dataerrorisLoadingisValidating を fetcher 関数の状態に応じて返す。
公式ドキュメントでいくつかのパターンが紹介されている。

1. フェッチして再検証する

  • 最初のデータ取得では、取得が終わるまでdataundefinedになっている
  • 再検証する際、最初に取得したデータを表示しながら検証することができ、検証が終わると表示が切り替わる
  • ユーザに何度もロード画面を表示することなく、最新データを表示できるのでUXがいい

2. フェッチした後キーを変更し、再検証する

  • キーを変更すると、再度ローディングと検証が行われる
  • その間、データはundefinedになる

3. フェッチした後キーを変更し、再検証する(keepPreviousDataオプション)

  • keepPreviousData オプションをつけることで、二回目のデータ取得の際に以前のデータを表示し続けることができる
  • データがundefinedにならないので、UXを改善できる

4. フェッチした後に再検証するのをフォールバックデータと一緒に行う

  • SSR や SSG を使用している時などでは、useSWR()でリクエストする前に既にデータを取得していることがある。そのような場合、fallbackDataオプションを使うことで、あらかじめ初期値を設定することができる。
// "hoge"を`'/api/data'`の初期値として設定する
useSWR('/api/data', fetcher, { fallbackData: "hoge" })

5. フェッチした後にキーを変更し再検証するのをフォールバックデータと一緒に行う

  • <SWRConfig />のfallbackオプションを使うことで、より広範囲かつ柔軟に初期値を設定できる
<SWRConfig value={{
  fallback: {
    '/api/user': { name: 'Bob', ...  },
    '/api/items': {...},
    ...
  }
}}>
  <App/>
</SWRConfig>

6. フェッチした後に再検証するのをフォールバックデータと一緒に行う(keepPreviousData)

よりよいUXのために

よりよいUXのためにはisValidatingisLoadingの特性をよく知っておき、使い分ける必要がある

  • isValidatingは実行中のリクエストがある場合に データがロード済みかどうかに関わらずtrueになる
  • isLoadingは実行中のリクエストがありロード済みのデータがまだない場合 にtrueになる

また、keepPreviousData を使うことで以前のデータを維持することによりユーザー体験を改善することができる。

omorimoriomorimori

グローバルな設定

https://swr.vercel.app/ja/docs/global-configuration

SWRConfigコンテキストによって囲んだコンポーネント内のすべての SWR フックに対して、グローバルな設定を提供できる。

<SWRConfig value={options}>
  <Component/>
</SWRConfig>

設定のネスト

設定がネストされている場合、親で指定された設定を子にマージする。
設定は関数でもオブジェクトでも記述することができる。

公式ドキュメントより引用
import { SWRConfig, useSWRConfig } from 'swr'
 
function App() {
  return (
    <SWRConfig
      value={{
        dedupingInterval: 100,
        refreshInterval: 100,
        fallback: { a: 1, b: 1 },
      }}
    >
      <SWRConfig
        value={{
          dedupingInterval: 200, // これはプリミティブ値であるため親の値を上書きします
          fallback: { a: 2, c: 2 }, // これはマージ可能なオブジェクトであるため親から受け取った値とマージします
        }}
      >
        <Page />
      </SWRConfig>
    </SWRConfig>
  )
}
 
function Page() {
  const config = useSWRConfig()
  // {
  //   dedupingInterval: 200,
  //   refreshInterval: 100,
  //   fallback: { a: 2,  b: 1, c: 2 },
  // }
}

グローバル設定へのアクセス

useSWRConfigフックを使ってグローバル設定、およびミューテーションとキャッシュを取得できる

公式ドキュメントより引用
import { useSWRConfig } from 'swr'
 
function Component () {
  const { refreshInterval, mutate, cache, ...restConfig } = useSWRConfig()
 
  // ...
}
omorimoriomorimori

エラーハンドリング

フェッチの promise がリジェクトされた場合、 error オブジェクトが定義される

const fetcher = url => fetch(url).then(r => r.json())
 
// ...
const { data, error } = useSWR('/api/user', fetcher)

ステータスコードとエラーオブジェクト

APIからステータスコードやエラーオブジェクトが返ってくると、その後のエラー処理を柔軟に行うことができる。
そんな時はfetcherをカスタマイズすることで、より多くの情報を返すことができる。

下記のようにfetcherの中でAPIのレスポンスを解析し、エラー時のエラーオブジェクトを設定することができる。

const fetcher = async url => {
  const res = await fetch(url)
 
  // もしステータスコードが 200-299 の範囲内では無い場合、
  // レスポンスをパースして投げようとします。
  if (!res.ok) {
    const error = new Error('An error occurred while fetching the data.')
    // エラーオブジェクトに追加情報を付与します。
    error.info = await res.json()
    error.status = res.status
    throw error
  }
 
  return res.json()
}
 
// ...
const { data, error } = useSWR('/api/user', fetcher)
// error.info === {
//   message: "You are not authorized to access this resource.",
//   documentation_url: "..."
// }
// error.status === 403

エラー時の再試行

  • SWRはExponential backoffというアルゴリズムで、エラー時にリクエストを再試行する。
  • onErrorRetryオプションでエラー時の動作をオーバーライドできる。たとえば特定のエラーステータスの時はリトライしないなど
  • グローバル設定にすることもできるので、プロジェクトとしてエラー処理の方針を統一することもできる

Exponential backoffとは

Exponential backoffの日本語解説記事
3行でまとめると

  • リトライの際に一定間隔で処理を再試行すると、通信を受け取る側のサーバーに大きな負荷がかかる可能性がある
  • Exponential Backoffではクライアントが通信に失敗した際に要求間の遅延を増やしながら定期的に再試行するアプローチ
  • サーバの負荷を軽減したり、無駄なリクエストも省くことができ、結果的にリトライ成功可能性が上がる

グローバルエラーの報告

onError(err, key, config): リクエストがエラーを返したときのコールバック関数

onErrorをグローバル設定で定義することで、リクエストがエラーを返したときにUIで表示したり、監視ツールに報告したりといった共通の処理を実装することができる。

omorimoriomorimori

自動再検証

フォーカス時の再検証

  • ページにフォーカスを合わせるかタブを切りかえると、SWR は自動的にデータを再検証する
  • 古いモバイルタブやスリープ状態になったラップトップなどのシナリオでデータを更新してくれるのでUXがよい

定期的な再検証

  • refreshIntervalオプションを設定することで、該当のコンポーネントが画面に表示されている時だけ、自動で定期的に再フェッチが行われる

再接続時の再検証

  • データが常に最新であることを確認するために、SWR はネットワークが回復したときに自動的に再検証する
  • デフォルトONだが、revalidateOnReconnect オプションで無効にできる

自動再検証の無効化

  • リソースがイミュータブルで、検証の必要がない場合は、そのリソースの全ての種類の自動再検証を無効にできる。
  • SWR はリソースをイミュータブルとして扱うヘルパーフックの useSWRImmutable を提供している
  • 上記は通常のuseSWRと同じAPIインターフェースを備えているが、一度データがキャッシュされると、二度とリクエストされなくなる。
import useSWRImmutable from 'swr/immutable'
 
// ...
useSWRImmutable(key, fetcher, options)
omorimoriomorimori

条件付きフェッチ

以下のような書き方をすることで、フェッチ時の条件を指定できる

// 条件付きでフェッチする
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
 
// ...または、falsyな値を返します
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
 
// ...または、user.id が定義されてない場合にスローします
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)

依存性

  • 他のデータに依存するデータをフェッチすることもできる
  • データフェッチが別の動的なデータに依存している場合は順番に実行することもできる
function MyProjects () {
  const { data: user } = useSWR('/api/user')
  const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
  // 関数を渡す場合、SWRは返り値を`key`として使用します。
  // 関数がスローまたはfalsyな値を返す場合、
  // SWRはいくつかの依存関係が準備できてないことを知ることができます。
  // この例では、`user.id`は`user`がロードされてない時にスローします。
 
  if (!projects) return 'loading...'
  return 'You have ' + projects.length + ' projects'
}
omorimoriomorimori

引数

https://swr.vercel.app/ja/docs/arguments

よくある認証APIにTokenを渡す時の書き方は以下

const { data: user } = useSWR(['/api/user', token], ([url, token]) => fetchWithToken(url, token))

KeyにはURLとトークンの組み合わせを指定している。

オブジェクトの受け渡し

const { data: user } = useSWR(['/api/user', token], fetchWithToken)
const { data: orders } = useSWR({ url: '/api/orders', args: user }, fetcher)

上記はオブジェクトをKeyにしており、これは内部で自動的に文字列にシリアライズされている。

omorimoriomorimori

ミューテーションと再検証

https://swr.vercel.app/ja/docs/mutation

ミューテーションはローカルデータの更新のこと。フェッチしたデータをローカルで更新できる。
再検証はSWRがデータを自動的に再取得する機能のこと。

ミューテーションと再検証は異なる概念であり、ミューテーションは主にデータの変更や更新、ローカルデータの操作に関する機能です。一方、再検証はキャッシュの有効期限管理や最新のデータ取得の自動化に関する機能です。これらの機能を組み合わせることで、SWRは効果的なデータ管理を実現します。

mutate

mutate API を使いデータをミューテートするには 2 つの方法がある。

  • グローバルミューテート API : どんなキーに対してもミューテートできる
    • useSWRConfig()の返り値から取得可能
    • keyが紐づけられていないので、引数で指定する必要がある
  • バウンドミューテート API : 対応する SWR フックのデータのみミューテートできる
    • useSWRの返り値から取得可能
    • keyが紐づいているので、引数で指定する必要がない

これらのmutateAPIに更新したい内容を渡すことで、SWRにキャッシュの更新を通知できる。
さらに、SWRはデータのキャッシュとUIの同期を自動的に行うため、該当コンポーネントが自動で更新される。

再検証

mutatenをデータの指定なしに呼んだ場合、そのリソースに対して再検証を発行する(データを期限切れとしてマークして再フェッチを発行する)。

返り値

dataパラメータとして扱える値を返し、エラーをキャッチすることもできる

try {
  const user = await mutate('/api/user', updateUser(newUser))
} catch (error) {
  // ユーザーの更新中に発生したエラーを処理します
}

useSWRMutation

このAPIでのミューテーションは、自動的にミューテーションを行う useSWR などとは異なり手動でのみ発行される。

基本的な使い方は以下。返り値としてtrigger(arg, options)というリモートミューテーションを発行するための関数がもらえる。
以下の例ではボタン押下時にミューテーションがトリガーされるようになっている。

import useSWRMutation from 'swr/mutation'
 
async function sendRequest(url, { arg }: { arg: { username: string } }) {
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg)
  }).then(res => res.json())
}
 
function App() {
  const { trigger, isMutating } = useSWRMutation('/api/user', sendRequest, /* options */)
 
  return (
    <button
      disabled={isMutating}
      onClick={async () => {
        try {
          const result = await trigger({ username: 'johndoe' }, /* options */)
        } catch (e) {
          // エラーハンドリング
        }
      }}
    >
      Create User
    </button>
  )
}
  • useSWRMutation は trigger が呼ばれるまでリクエストを開始しないので、データが実際に必要になるまでデータの読み込みを遅延することができる。

楽観的更新optimisticData

optimisticData オプションを使うことでデータ更新時、サーバの変更の前にUIを更新できる。
ローカルデータをミューテーションすることは、変更を早く感じさせるのでUX的に良いこと!

例.これまでのTODOリストを更新してから表示するまでの流れ

  1. TODOを追加
  2. バックエンドが更新される
  3. mutateを通してサーバにリクエストを送信
  4. 返ってきたレスポンスのデータを使って更新

上記の例だと、更新に時間がかかった場合表示までに時間がかかり、UXが良くない
下記のように、optimisticData オプションを使うことで、サーバからのレスポンスを待つことなく新しい状態のリストを表示できる。data を optimisticData の値によって更新し、サーバにリクエストを送信。リクエストが完了したら SWR はリソースを revalidate しデータが最新であることを保証する。

const { mutate, data } = useSWR('/api/todos')

return <>
  <ul>{/* データの表示 */}</ul>

  <button onClick={() => {
    mutate(addNewTodo('New Item'), {
      // クライアントキャッシュを即座に更新する
      optimisticData: [...data, 'New Item'],
    })
  }}>
    Add
  </button>
</>

こちらの解説記事が非常に参考になった

エラー時のロールバック

optimisticDataを使った場合、より早くユーザに更新後のデータを表示できるが、その更新が失敗した場合はユーザが見ている画面が正しくないということが起こってしまう。
rollbackOnErrorを有効にしてローカルキャッシュに対する変更を取り消して以前のデータに戻すことでそれを防ぐことが可能。

ミューテーション後にキャッシュを更新

populateCacheオプションを有効にすることで、ミューテーションのレスポンスで useSWR のキャッシュを更新できる。

const { mutate, data } = useSWR('/api/todos')

return <>
  <ul>{/* データの表示 */}</ul>

  <button onClick={() => {
    mutate(addNewTodo('New Item'), {
      optimisticData: [...data, 'New Item'],
      // ミューテーションのレスポンスで useSWR のキャッシュを更新
      populateCache: true,
    })
  }}>
    Add
  </button>
</>

レースコンディションの回避

ミューテーションがuseSWRの再フェッチと同時に発生する可能性もある。
例えはgetUserを先に開始したものの、updateUserより時間がかかってしまうケース。
このとき、useSWRMutationはuseSWRの再フェッチを自動的に破棄してくれるので、古いデータは表示されない。

omorimoriomorimori

ページネーション

https://swr.vercel.app/ja/docs/pagination

ページネーション

ページネーションや無限ローディングなどの一般的な UI パターンをサポートするための専用 API である useSWRInfinite を提供している。

function Page ({ index }) {
  const { data } = useSWR(`/api/data?page=${index}`, fetcher);
 
  // ... ローディングとエラー状態を処理します
 
  return data.map(item => <div key={item.id}>{item.name}</div>)
}
 
function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  return <div>
    <Page index={pageIndex}/>
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

SWR のキャッシュがあるため、次のページを事前にロードできるという利点があります。
と公式には記載があるが、上の実装だと初回に1ページ目を見ている時に2ページ目が自動でフェッチされるということはないはず。2ページ目に居て1ページ目に戻るときはキャッシュを使用して高速に表示される(同時に再検証が走る)

useSWRInfinite

useSWRInfinite は、一つのフックで多数のリクエストを開始する機能を提供していて、ページネーションや無限ローディングの実装に向いている

import useSWRInfinite from 'swr/infinite'
 
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)

引数のgetKeyが肝で、各ページのSWRキーを取得する関数になっている。
これがフェッチの前に実行され、fetcherの引数になる。NULLの場合、フェッチしない。

わかりやすいZennの記事

omorimoriomorimori

サブスクリプション

https://swr.vercel.app/ja/docs/subscription

useSWRSubscription はリアルタイムのデータソースを SWR でリアルタイムな更新を管理するための React フック
useSWR フックとは異なり、リアルタイムなデータの変更を取得する場合に使用する。これにより、データの変更が発生したときにコンポーネントがリアルタイムに更新されることが可能となる。

基本
useSWRSubscription<Data, Error>(key: Key, subscribe: (key: Key, options: { next: (error?: Error | null, data: Data) => void }) => () => void): { data?: Data, error?: Error }
  • key:useSWR キーと同じキー。
  • subscribe:リアルタイム・データ・ソースをsubscribeする関数。以下の引数を受け取る:
    • key: 上記と同じキー。
    • options: 以下のプロパティを持つオブジェクト:
      • next:エラーとデータを受け取り、リアルタイム・データ・ソースから受け取った最新のデータで状態を更新する関数。

ユースケースとしては以下があげられそう

  1. チャットアプリやリアルタイム通知などのリアルタイムなデータの更新
  2. ダッシュボードやグラフなどのリアルタイムのデータの可視化

firestoreやWebSocketなどと組み合わせて使うのが良さそう

重複排除

useSWRSubscription は同じキーを用いた購読リクエストに対して重複排除を行っているので、複数のコンポーネントで同じキーを使っている場合、処理を共有してくれる。
また、キー毎に一つだけリアルタイム更新の管理を行うので、余計なリクエストを減らせる

omorimoriomorimori

プリフェッチ

https://swr.vercel.app/ja/docs/prefetching

プリフェッチとは、ユーザーがウェブページを閲覧する際に、将来必要になるかもしれないリソース(通常はページへのリンクやリソース)を事前にダウンロードまたはキャッシュしておくプロセスやテクニックを指す。

公式によると、一番おすすめなのはTop-Levelでやること。
以下のようにHeaderの中でデータを優先的に取得することが可能。

<link rel="preload" href="/api/data" as="fetch" crossorigin="anonymous">

プログラムによるプリフェッチ

SWR は preload というデータをプログラマブルにプリフェッチして結果をキャッシュに保存する API を提供しています。preload は key と fetcher を引数として受け取ります。

以下のように、コンポーネントのレンダリング前に呼ぶことで素早く描画できる

import { useState } from 'react'
import useSWR, { preload } from 'swr'
 
const fetcher = (url) => fetch(url).then((res) => res.json())
 
// User コンポーネントがレンダリングされる前にプリロードします
// これによりウォーターフォール問題の発生を避けられます
// また、ボタンやリンクにホバーされたタイミングでプリロードを開始することもできます
preload('/api/user', fetcher)
 
function User() {
  const { data } = useSWR('/api/user', fetcher)
  ...
}
 
export default function App() {
  const [show, setShow] = useState(false)
  return (
    <div>
      <button onClick={() => setShow(true)}>Show User</button>
      {show ? <User /> : null}
    </div>
  )
}

ボタンのイベントコールバックの中などでプリロードすることができる

function App({ userId }) {
  const [show, setShow] = useState(false)
 
  // エフェクトの中でプリロードする
  useEffect(() => {
    preload('/api/user?id=' + userId, fetcher)
  }, [userId])
 
  return (
    <div>
      <button
        onClick={() => setShow(true)}
        {/* イベントコールバックの中でプリロードする */}
        onHover={() => preload('/api/user?id=' + userId, fetcher)}
      >
        Show User
      </button>
      {show ? <User /> : null}
    </div>
  )
}

レンダリング前やページ遷移前に読んでおくことでUXを向上できる

omorimoriomorimori

サスペンス

React v18でサスペンス機能が追加された。
どんな機能か簡単に言うと

サスペンスにより、コンポーネントツリーの一部がまだ表示できない場合に、ロード中という状態を宣言的に記述できるようになります

と言うことらしい。サスペンス機能を適切に分割して使うことでUXを向上できる。

  • データ取得中のハンドリングをいちいち分岐で書かなくてよくなるので便利そう。
  • レンダリング時にデータがあることを保証できる

Suspenseの基本的な使い方や内部仕様を簡単に解説している記事があったのでメモ
https://zenn.dev/moyongkexing/articles/66d676a79a180b

SWRとサスペンス

機能は提供されているが、React側では推奨ではない
https://swr.vercel.app/ja/docs/suspense