SWR とは
useSWR フック
SWR (useSWR
) は、Next.js を開発している人たち (Vercel) が開発したデータフェッチ用の React フックライブラリです。
React アプリでデータフェッチを真面目に実装しようとすると、大体最後にこのライブラリに行き着きますので、最初からこれを使いましょう(GraphQL の場合は Apollo Client がありますが)。
React コンポーネント内から Web API などを呼び出してデータフェッチを行う場合、標準の仕組みだけで実現しようとすると、useEffect
フックなどで fetch
関数を呼び出したりすることになります。
function Page () {
const [user, setUser] = useState(null)
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data))
}, [])
// ...
}
もちろん、それで実現は可能なのですが、「データ取得前の表示」「エラー処理/リトライ処理」「非同期処理のキャンセル処理」などをちゃんとやろうとすると非常に煩雑なコードになってきます(上記コードはそれらが全く考慮できていません)。
useSWR
フックを使うと、クライアントサイド JavaScript からのデータフェッチ処理をとても綺麗に記述することができます。
import { FC } from 'react'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then((r) => r.json())
const Profile: FC = () => {
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>
}
export default FC
useSWR の特徴
useSWR
は高度なキャッシュコントロール機能を備えています。
- キー名によるキャッシュ共有
- 第1パラメータのキー名を共通にするだけで、ページ間でキャッシュを共有できます。
useEffect
などでデータフェッチを行おうとすると、無駄なフェッチを防ぐために、トップレベルのコンポーネントで一度だけデータ取得して子コンポーネントにたらい回ししていく、という実装になりがちです。useSWR
であれば、各コンポーネントで個別にデータフェッチする感覚で呼び出せます。 - 自動リトライ、ポーリング
- エラー発生時のリトライ処理 (exponential backoff アルゴリズム)、ネットワーク復帰時の自動フェッチ (
{ revalidateOnReconnect: true }
)、ブラウザのタブ切り替え時の自動フェッチ ({ revalidateOnFocus: true }
) などをデフォルトで行ってくれるため、ユーザーが最新データにアクセスしやすくなります。これらの設定はuseSWR
呼び出し時のオプションで制御できます(参考: useSWR のオプション)。 - 手動でのキャッシュ更新(ミューテート)
useSWR
フックは、任意のタイミングでキャッシュ更新をかけるためのミューテート関数 (mutate
) を提供してくれます。キャッシュが更新されると、自動的にコンポーネントは再描画されます。また、グローバルなmutate
API も提供されており、指定したキーのキャッシュをどこからでも更新できます。- フェッチ関数は自由
useSWR
フックは、デフォルトでフェッチ関数としてfetch
を使用(JSONデータを想定)しますが、Promise
を返す任意のフェッチ関数を使用することができます。- TypeScript、Next.js 対応
- Vercel 製なので、もちろん Next.js に対応していて、プリレンダリング時にも呼び出せます。Web サイトビルド時にはその時点でのデータで静的なページを生成しておいて、実行時は最新データをフェッチ&キャッシュするといったことが可能です。TypeScript の型情報も標準でサポートしています。
(コラム) SWR = stale-while-revalidate
SWR という名前の由来は、RFC5861 で提唱されている HTTP レスポンスヘッダの Cache-Control
拡張のひとつである stale-while-revalidate から来ています。
Cache-Control
はデータフェッチ後のキャッシュ有効期間を示すために導入されたもので、例えば、Cache-Control: max-age=600
が返された場合は、ブラウザは 600 秒間キャッシュを保持します(その間のデータフェッチ要求ではサーバーアクセスしない)。
そして、600 秒経過するとキャッシュは古い (stale) とみなされて、次のデータフェッチ時には実際にサーバーアクセスが発生します。
このとき、データフェッチ処理が同期的に行われるため、ユーザーはデータ表示まで少し待たされることになります。
この待ち時間を軽減しようというのが stale-while-revalidate (SWR) という拡張で、例えば、
Cache-Control: max-age=600, stale-while-revalidate=300
というレスポンスが返されると、max-age
の 600 秒経過後も、stale-while-revalidate
で指定された 300 秒間は、古いキャッシュ (stale cache) がブラウザでの描画に使われます。
そして、その背後でサーバーに対してデータフェッチを行うことで、キャッシュの更新も同時に行います。
この仕組みにより、(多少古いかもしれない)キャッシュでとりあえず高速に初期表示しつつ、常に最新のデータをキャッシュしておく、ということが実現できます。
React ライブラリの useSWR
フックもこの思想をもとに実装されており、ブラウザ側のキャッシュを使って高速な表示をしつつ、背後でキャッシュの更新を行って必要に応じて再描画を行う、といったことを可能にしています。
useSWR
フックのすごいところは、この辺りの複雑なキャッシュコントロールを意識せずに、シンプルにデータフェッチ処理を記述できるところにあります。
SWR のインストール
SWR フックライブラリは yarn
あるいは npm
で簡単にインストールできます。
### yarn の場合
$ yarn add swr
### npm の場合
$ npm install swr
これで、次のように useSWR
関数をインポートできるようになります。
import useSWR from 'swr'
useSWR 関数の使い方
基本的な使い方
下記のコンポーネントは、GitHub のユーザー情報を REST API を使って取得&表示します。
import { FC } from 'react'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then((r) => r.json())
type Props = { login: string } // コンポーネントの props の型
type Data = { name: string } // フェッチするデータの型
export const GitHubUser: FC<Props> = ({ login }: Props) => {
const url = `https://api.github.com/users/${login}`
const { data, error } = useSWR<Data, Error>(url, fetcher)
if (error) return <div>Failed to load</div>
if (!data) return <div>Loading...</div>
return <div>Name: {data.name}</div>
}
// 使用例: <GitHubUser login="octocat" />
useSWR
関数の第1パラメーターは、フェッチ関数に渡すパラメーターであり、キャッシュを識別するためのキーとしても使われます。
典型的には Web API の URL を渡しますが、配列の形で付加データを渡すことも可能です。
戻り値の data
プロパティには、フェッチ関数によって取得したデータ(Promise.resolve()
された値)が格納されますが、データ取得が完了するまでは data
の値は undefined
になります。
なので、ロード中であるかどうかを if (!data)
で簡単に判別できます(isLoading
とか存在しません)。
error
プロパティも同様で、エラーが発生していなければ undefined
になり、エラーが発生したらエラー内容(Promise.reject()
された値)が格納されます。
フェッチ関数を指定する
useSWR
の第2パラメーターでは、データフェッチに使用するフェッチ関数を指定することができます。
下記は、JSON データを返す Web API を呼び出すときに使う典型的なフェッチ関数の使用例です。
const fetcher = (url: string) => fetch(url).then((r) => r.json())
export const Hello: FC = () => {
// ...
const { data, error } = useSWR<Data, Error>(url, fetcher)
// ...
}
フェッチ関数と言うと特殊なもののように聞こえますが、単なる getter 関数であり、Promise<Xxx>
を返す非同期関数であれば、そのままフェッチ関数として使えると考えればよいです。
次の例では、fetchBookData
関数をフェッチ関数として使用しています。
この fetchBookData
関数はそれ単独でも使えるものですが、それをそのまま useSWR
に渡すことができます。
import useSWR from 'swr'
type BookData = {
author: string
titles: string[]
}
async function fetchBookData(author: string): Promise<BookData> {
try {
// ...本当はここで fetch 処理など
const bookData = {
author: author,
titles: ['Title1', 'Title2', 'Title3'],
}
return bookData
} catch (err) {
// Error を再スローするだけなら、通常は try ... catch は必要ない
throw err
}
}
const BookList: React.FC = () => {
const { data: bookData, error } = useSWR<BookData, Error>('Author1', fetchBookData)
if (error) return <p>Error: {error.message}</p>
if (!bookData) return <p>Loading...</p>
return (
<ul>
{bookData.titles.map((title) => (
<li key={title}>
{bookData.author} - {title}
</li>
))}
</ul>
)
}
パラメーターが 2 つ以上の関数をフェッチ関数として使いたい場合は、useSWR
の第1パラメータ―に引数を配列でまとめて渡します。
フェッチ関数側に配列が渡されるわけではないことに注意してください。
// パラメーターが 2 つ必要なフェッチ関数
async function fetchData(key1: string, key2: string): Promise<Data> {
// ...
}
// useSWR 経由で上記関数を呼び出す
export const MyComponent: FC = () => {
const { data, error } = useSWR<Data, Error>(['aaa', 'bbb'], fetchData)
// ...
}
フェッチ関数の仕組みは非常に柔軟で、時間がかかるデータフェッチをシミュレートしたり、エラーが発生したときにどう見えるかを簡単に確認できます。
例えば次のフェッチ関数は 1 秒後に現在時刻を返すか、エラーを発生させます。
通常、フェッチ関数のパラメーターとして、useSWR
の第 1 引数で渡された値を受け取りますが、このフェッチ関数は特殊でパラメーターなしです。
const fetchCurrentTime = async () => {
// 1秒待つ
await new Promise((res) => setTimeout(res, 1000))
// 現在時刻の文字列を返すか、エラーを発生させる
if (Math.random() > 0.3) {
return new Date().toLocaleString()
} else {
throw new Error('An error has occurred!')
}
}
// 使用例
const { data, error } = useSWR<string, Error>('/fake', fetchCurrentTime)
フェッチ関数の中でスローした Error オブジェクトは、useSWR
関数の戻り値オブジェクトの error
プロパティにされます(エラーがしなかった場合は undefined
)。
フェッチ関数を省略する
SWRConfig コンポーネント を使用すると、子コンポーネント内で useSWR
を使用するときの共通設定(グローバル設定)を行うことができます。
具体的には、useSWR
の第 3 引数に渡すオプションオブジェクトのデフォルト値として扱われます。
このオブジェクトには、次のようにのフェッチ関数 (fetcher
) を指定しておくことができます。
import type { AppProps } from 'next/app'
import { SWRConfig } from 'swr'
/** Default fetcher for useSWR hook. */
const fetcher = (url: string) => fetch(url).then((r) => r.json())
export default function MyApp({ Component, pageProps }: AppProps): JSX.Element {
return (
<SWRConfig value={{ fetcher }}>
<Component {...pageProps} />
</SWRConfig>
)
}
これで、子コンポーネントから useSWR
関数を呼び出すときにフェッチ関数を省略できるようになります。
const { data: projects } = useSWR('/api/projects')
useSWR はカスタムフックにラップして使う
useSWR
特有の話ではないですが、データフェッチ処理に関してはコンポーネントの実装から分離して、カスタムフックの形で定義しておくと保守性の高いコードになります。
次の useIssueCount
カスタムフックは、指定した GitHub リポジトリ ($org/$repo
) のイシュー数を取得します。
import useSWR from 'swr'
type UseIssueCountInput = {
org: string
repo: string
}
type UseIssueCountOutput = {
count?: number
error?: Error
isLoading: boolean
}
const fetcher = (url: string) => fetch(url).then((r) => r.json())
export const useIssueCount = (
input: UseIssueCountInput
): UseIssueCountOutput => {
const { org, repo } = input
const url = `https://api.github.com/repos/${org}/${repo}/issues`
const { data, error } = useSWR<[], Error>(url, fetcher)
return {
count: data?.length,
error,
isLoading: !error && !data,
}
}
そして描画を担うコンポーネント側の実装では、データ取得処理はカスタムフックの呼び出しだけで済ませます。 カスタムフックを呼び出している時点で、データ取得処理が完全に分離できているとは言えませんが、こうすることで「見た目」の定義に集中できます。
import { FC } from 'react'
import { useIssueCount } from '../hooks/useIssueCount'
export const IssueCount: FC = () => {
const { count, error, isLoading } = useIssueCount({
org: 'vercel',
repo: 'swr',
})
if (error) return <div>Error</div>
if (isLoading) return <div>Loading...</div>
return <div>Count: {count}</div>
}
ちなみに、さらに「見た目」と「ロジック」を分離する手法に、Presentational component / Container component という方法があります。 これは、React コンポーネントを「見た目」用と「ロジック」用の 2 種類に分けて実装する考え方です。
- Presentational component … 純粋な UI コンポーネント。渡された
props
をそのまま表示するだけ。 - Cotainer component … ロジック(フックなどの呼び出し)を含むコンポーネント。内部でのデータ取得結果などを Presentational component に
props
で渡す。
応用
フェッチ関数に追加のデータを渡す
API 呼び出し時に、フェッチ関数にトークン情報を渡したいことがあるかもしれません。
そのような場合は、useSWR
関数のキーパラメータを配列にすることで、トークン情報が変化した時に再フェッチさせることができます。
const { data: user } = useSWR(['/api/user', token], fetchWithToken)
フェッチ関数 fetchWithToken
は、2 つのパラメーター(上の例であれば、/api/user
と token
)を受け取り、Promise<Xxx>
を返すように実装します。
パラメーターとして配列を受け取るわけではないことに注意してください(配列の要素が個別パラメーターとして渡されてきます)。
type Output = { user: string }
async function fetchWithToken(url: string, token: string): Promise<Output> {
// ...
return { user: 'Jojo' }
}
依存関係のある useSWR の連続呼び出し
useSWR
関数の第1パラメーターが偽とみなされる値 (falsy) 場合、フェッチ処理はスキップされます。
次の例では、この性質を利用して、1番目の useSWR
の結果を取得した段階で、2番目の useSWR
によるデータフェッチを発火させています。
それまでは、1番目の useSWR
の結果のみを使ってコンポーネントが描画されます。
const { data: user } = useSWR(['/api/user', token], fetchWithToken)
const { data: orders } = useSWR(user ? ['/api/orders', user] : null, fetchWithUser)
ちなみに、useSWR
は React フックなので、データフェッチが必要ないケースでも上記のように必ず呼び出す必要があります(条件分岐して呼び出し自体をスキップしてはいけません)。
フェッチ関数から独自のエラーを投げる
useSWR
関数の第 2 引数で指定した独自のフェッチ関数からエラーオブジェクトを throw すると、useSWR
関数の戻り値オブジェクトの error
プロパティとして受け取ることができます。
useSWR
関数を使用する箇所で、データフェッチに関する細かいエラー処理を行いたいのであれば、フェッチ関数の中で詳細情報を詰めたエラーオブジェクトを throw
すれば OK です。
// 独自のエラークラスの定義例
export class GameError extends Error {
errorId: string // 独自のエラー ID
statusCode: number // HTTP ステータスコード
constructor(errorId: string, statusCode: number, message?: string) {
// Error オブジェクト用の設定
super(message ?? `Error ${errorId} occurred.`)
this.name = new.target.name
// 独自のエラー情報
this.errorId = errorId
this.statusCode = statusCode
}
}
// 独自のフェッチ関数
export async function fetchGame(url: string): Promise<Game> {
const res = await fetch(url)
if (!res.ok) {
// ステータスコードが 2xx 以外の場合は、独自エラーを投げる
throw new GameError('FETCH_GAME_ERROR', res.status)
}
return (await res.json()) as Game
}
useSWR
を呼び出すときは、第 2 型パラメーターでエラーオブジェクトの型を指定できます。
const { data, error } = useSWR<Game, GameError>(url, fetchGame)
if (error) {
console.error(error.errorId) // FETCH_GAME_ERROR
console.error(error.statusCode) // 404 など
console.error(error.message) // Error FETCH_GAME_ERROR occurred.
}