useSWRとは
API通信に対して、レスポンス情報をキャッシュすることで、毎回API通信がされてしまうのを防ぐhooks。
公式によると、以下のようなメリットがある。
- 速い、 軽量 そして 再利用可能 なデータの取得
- 組み込みの キャッシュ とリクエストの重複排除
- リアルタイム な体験
- トランスポートとプロトコルにとらわれない
- SSR / ISR / SSG support
- TypeScript 対応
- React Native
使い方
公式ドキュメント
基本的な使い方
import useSWR from 'swr';
type User = {
id: string;
name: string;
email: string;
}
async function fetcher(key: string) { // keyはuseSWR()の第1引数で渡されたURL
return fetch(key).then((res) => res.json() as Promise<User | null>);
}
export const App = () => {
const { data, error, isLoading } = useSWR('https://jsonplaceholder.typicode.com/users/1', fetcher);
if (error) return <div>エラーです</div>;
if(isLoading) return <div>読み込み中...</div>;
return (
<div>
<h1>名前:{data?.name}</h1>
<h1>メール:{data?.email}</h1>
</div>
);
}
data
にはfetcher
の返り値が入る。同時に、error
と現在読み込み中かを表すisLoading
を取得できるので、それぞれがtrueのときは別の画面を表示するようにしてある。
公式ドキュメントのクイックスタートではfetcher
は非同期関数になっていないが、実際に利用する際は上記の例のように、非同期関数にすることを推奨する(fetcher
は結局のところネイティブの fetch
をラップした関数なので、いつもfetch
を使うノリでPromiseを返すようにしておきましょう)。
fetcher
は以下のように定義することもできる。以下3つとも同じ動作をする。
useSWR('/api/user', () => fetcher('/api/user'))
useSWR('/api/user', url => fetcher(url))
useSWR('/api/user', fetcher)
fetcher
は最終的に必要なデータを取得して適切な形式で渡す非同期関数であればいいので、ネイティブのfetch
を使う必要はなく、axios
などを利用することもできる。
import axios from 'axios'
const fetcher = url => axios.get(url).then(res => res.data)
データの事前fetch
上記のコードでは該当するコンポーネントを表示しない限り、データのfetchは当然行われない。だがpreload
を使うと事前にデータをfetchしておく、ということもできる。
import useSWR from 'swr';
import { preload } from 'swr';
type User = {
id: string;
name: string;
email: string;
}
async function fetcher(key: string) { // keyはuseSWR()の第1引数で渡されたURL
return fetch(key).then((res) => res.json() as Promise<User | null>);
}
// データの事前読み込み(当然キャッシュに保存される)
plelaod('https://jsonplaceholder.typicode.com/users/1', fetcher);
export const App = () => {
const { data, error, isLoading } = useSWR('https://jsonplaceholder.typicode.com/users/1', fetcher);
if (error) return <div>エラーです</div>;
if(isLoading) return <div>読み込み中...</div>;
return (
<div>
<h1>名前:{data?.name}</h1>
<h1>メール:{data?.email}</h1>
</div>
);
}
hooksにまとめる
各component全てにfetcher
を書くのは面倒なので、hooksとしてまとめるのが良い。
基本的な使い方で示した例をhooksに分割すると以下のようになる。
(以降の例ではpreload
は除いてある)
// App.tsx
import { useUsers } from './hooks/useUsers';
export const App = () => {
const { user, isLoading, isError } = useUsers(1);
if (isError) return <div>エラーです</div>;
if(isLoading) return <div>読み込み中...</div>;
return (
<div>
<h1>名前:{user?.name}</h1>
<h1>メール:{user?.email}</h1>
</div>
);
}
// useUser.ts
import useSWR from 'swr';
type User = {
id: string;
name: string;
email: string;
}
async function fetcher(key: string) {
return fetch(key).then((res) => res.json() as Promise<User | null>);
}
export const useUsers = (id: number) => {
const { data, error, isLoading } = useSWR(`https://jsonplaceholder.typicode.com/users/${id}`, fetcher);
return {
user: data,
isLoading,
isError: error,
}
}
user
の情報を取得するロジックを抽象化し、useUsers.ts
にまとめた。これで他のコンポーネントでuser情報を使いたくなった場合はconst { user, isLoading, isError } = useUsers(1);
のように呼び出せばいい。1行書けばOKになったので、かなり簡単である。
エラーハンドリング
const { data, error, isLoading } = useSWR('hoge', fetcher);
fetcher
でrejectされた場合、error
オブジェクトに値が入ることになる(errorがない場合はundefined)。
必ずrejectが起きるfetcher
を作って確認してみる。
async function fetcher(key: string) {
return new Promise<User | null>((resolve, reject) => {
fetch(key)
.then((res) => {
reject(new Error(`errorが発生しました`)); // 必ずreject
})
});
}
const { data, error, isLoading } = useSWR(`https://jsonplaceholder.typicode.com/users/${id}`, fetcher);
console.log(error); // errorが発生しました
errorが発生しているとき、リクエストの再試行が行われる。再試行にはexponential backoff アルゴリズムが使われている。これにより、効率的なリクエストの再試行を実現している。
再試行の設定はonErrorRetry
で上書きすることもできる。
useSWR('/api/user', fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// 404では再試行しない。
if (error.status === 404) return
// 特定のキーでは再試行しない。
if (key === '/api/user') return
// 再試行は10回までしかできません。
if (retryCount >= 10) return
// 5秒後に再試行します。
setTimeout(() => revalidate({ retryCount }), 5000)
}
})
データの検証
useSWRは取得したデータをキャッシュすることで高速化を実現している。だがキャッシュされたデータが古くなってしまった場合、適切な情報をクライアントに渡すことができなくなってしまう。そこでuseSWRでは、データの自動検証が行われている。
検証されるタイミングは
- フォーカスしたとき再検証
- 設定された時間時間が経過したら再検証
- ユーザーがオンラインに戻ったとき再検証
などがある
フォーカスしたとき再検証
タブを切り替えた時などにデータを検証する。以下がビジュアル的にもわかりやすい。
設定された時間が経過したら再検証
ある程度の時間が経過したらデータを検証して、必要に応じて再びデータのfetchを行うことができる。検証対象はuseSWRで管理されているデータのうち、現在表示されているコンポーネントに関係があるもの。それ以外は検証の対象外になる。
設定は以下の通り。
const { data, error, isLoading } = useSWR(
`https://jsonplaceholder.typicode.com/users/${id}`,
fetcher,
{
refreshInterval: 1000, // 検証時間(ms)。デフォルトでは無効。
},
);
ユーザーがオンラインに戻ったとき再検証
ユーザーがコンピューターのロックを解除したときなどに自動的に再検証を行う。
キャッシュのカスタマイズ
デフォルトでは、 SWR はグローバルキャッシュを使用して、すべてのコンポーネント間でデータを保存および共有する。
ただ、SWRConfig
の provider
オプションを使うことでこれをカスタマイズすることができる。
上記のように、キャッシュをグローバルではないものとして扱うことができる。
providerの生成は以下のように行う。
import useSWR, { SWRConfig } from 'swr'
function App() {
return (
<SWRConfig value={{ provider: () => new Map() }}>
<Page/>
</SWRConfig>
)
}
キャッシュの更新(ミューテーション)
mutate
を使うことで、データのfetchを行わずにキャッシュを更新することができる。addやeditを行ったとき、わざわざfetchしなくてもキャッシュを更新できるので便利。reduxでいうグローバルstateの更新だと考えるとわかりやすい。reduxでいうstateが、useSWRだとキーというものになる。キーは具体的にはfetcherに渡したエンドポイント(例えばhttps://jsonplaceholder.typicode.com/users/1
)である。useSWRではfetchを実行する前に、キーに対応するキャッシュがある場合は、これを返すことになる。
バウンドミューテート
現在のキーのデータをmutateする。以下は「更新」ボタンをクリックするとmutate
の第1引数に渡しているオブジェクトの内容にキャッシュを更新する例である。
mutate
の第2引数にfalseを設定しないとキャッシュの再検証が行われる。つまりデータのfetchが自動的にされてしまう。キャッシュ更新後に再検証を行うことで、キャッシュのデータが正しいことが保証されるというメリットがあるが、意図せず通信回数を増やしてしまわないよう、適宜falseを設定しておきたい。
// usePosts.ts
export const usePosts = (id: number) => {
const { data, error, isLoading, mutate } = useSWR(`https://jsonplaceholder.typicode.com/posts/${id}`, fetcher);
return {
post: data,
isLoading,
isError: error,
mutate, // mutateをApp.tsxで呼び出せるようにする
}
}
// App.tsx
import { usePosts } from './hooks/usePosts';
import { updatePost } from 'hoge';
export const App = () => {
const { post, mutate } = usePosts(1);
const onClick = async () => {
const newPost = {
id: 1,
title: '更新後のタイトル',
body: '更新後の本文',
userId: 1,
}
await updatePost(newPost); // postを更新する仮のメソッド
mutate(newPost, false); // `https:/...com/posts/1`に紐づくキャッシュを更新する
}
return (
<div>
<p>タイトル:{post?.title}</p>
<p>本文:{post?.body}</p>
<button onClick={onClick}>更新</button>
</div>
);
}
グローバルミューテート
全てのキーに対して更新可能なmutate
を定義することができる。使い方は以下の通り。
import { useSWRConfig } from "swr"
export const App = () => {
const { mutate } = useSWRConfig();
mutate(key, data, options);
}
これまでの例の中で使ってみると、
import { usePosts } from './hooks/usePosts';
import { updatePost } from 'hoge';
import { useSWRConfig } from 'swr';
export const App = () => {
const id = 1;
const { post } = usePosts(id);
const { mutate } = useSWRConfig(); // グローバルなmutate
const onClick = async () => {
const newPost = {
id: 1,
title: '更新後のタイトル',
body: '更新後の本文',
userId: 1,
}
await updatePost(newPost); // postを更新する仮の非同期関数
// 引数は キー, 更新内容, 再検証を行うかどうか (falseで行わない)
mutate(`https://jsonplaceholder.typicode.com/posts/${id}`, newPost, false);
}
return (
<div>
<p>タイトル:{post?.title}</p>
<p>本文:{post?.body}</p>
<button onClick={onClick}>更新</button>
</div>
);
}
一つのmutate
で色々なキーのキャッシュについて更新ができるので便利ではあるが、第1引数に確実にキーを指定しなければならないので注意が必要。
なにがどうなったら再fetchが起きる?
再検証が行われるタイミングは上記の通りであるが、そもそも何を持って再fetchが必要と判断しているのかというと、更新前後のデータについて浅い比較を行い、変更がなければfetchしないと判断している。
// usePosts.ts
import useSWR from 'swr';
type Post = {
id: number;
title: string;
body: string;
userId: number;
}
async function fetcher(key: string) {
return fetch(key).then((res) => res.json() as Promise<Post | null>);
}
export const usePosts = (id: number) => {
const { data, error, isLoading, mutate } = useSWR(`https://jsonplaceholder.typicode.com/posts/${id}`, fetcher);
console.log('実行'); // consoleに実行と表示されないことがわかる
return {
post: data,
isLoading,
isError: error,
mutate,
}
}
// App.tsx
import { useEffect } from 'react';
import { usePosts } from './hooks/usePosts';
import { useUsers } from './hooks/useUsers';
import { useSWRConfig } from 'swr';
export const App = () => {
const id = 1;
const { post } = usePosts(id);
const { user, isLoading, isError } = useUsers(post?.userId);
const { mutate } = useSWRConfig();
const onClick = async () => {
// fetchを実行する前に更新前後のデータを比較(浅い比較?)していて、変更がなければfetcherは実行されない
// このnewPostは現在のpostと全く同じなので、再fetchはされない
const newPost = {
id: post?.id,
title: post?.title,
body: post?.body,
userId: post?.userId,
}
mutate(`https://jsonplaceholder.typicode.com/posts/${id}` ,newPost, false);
}
if (isError) return <div>エラーです</div>;
if(isLoading) return <div>読み込み中...</div>;
return (
<div>
<h1>名前:{user?.name}</h1>
<h1>メール:{user?.email}</h1>
<p>タイトル:{post?.title}</p>
<p>本文:{post?.body}</p>
<button onClick={onClick}>更新</button>
</div>
);
}
サブスクリプション
(調べてもあまり情報が出てこなかったので間違えている可能性が高いです・・・)
useSWRSubscription
を使うことで、サブスクリプションを実現する。
サブスクリプションとは、クライアントがサーバーをリアルタイムで監視し、サーバー側で変更があったらこれを検知して何らかの処理を行うことである。
(今後学習が進んだら追記する予定です。)
応用的な使い方
条件付きfetch
第一引数にnullもしくはfalsyな値を渡すことができる。その場合はfetchは実行されず、dataはundefinedのままになる。
// 条件付きでフェッチする
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)
依存関係のあるfetch
条件付きfetchを利用して、依存関係のあるfetchを簡単に実装することができる。
// App.tsc
import { usePosts } from './hooks/usePosts';
import { useUsers } from './hooks/useUsers';
export const App = () => {
const { post } = usePosts(1);
const { user, isLoading, isError } = useUsers(post?.userId); // postの取得が完了してからfetchされる
if (isError) return <div>エラーです</div>;
if(isLoading) return <div>読み込み中...</div>;
return (
<div>
<h1>名前:{user?.name}</h1>
<h1>メール:{user?.email}</h1>
<p>タイトル:{post?.title}</p>
<p>本文:{post?.body}</p>
</div>
);
}
// usePosts.ts
import useSWR from 'swr';
type Post = {
id: number;
title: string;
body: string;
userId: number;
}
async function fetcher(key: string) {
return fetch(key).then((res) => res.json() as Promise<Post | null>);
}
export const usePosts = (id: number) => {
const { data, error, isLoading } = useSWR(`https://jsonplaceholder.typicode.com/posts/${id}`, fetcher);
return {
post: data,
isLoading,
isError: error,
}
}
// useUsers.ts
import useSWR from 'swr';
type User = {
id: string;
name: string;
email: string;
}
async function fetcher(key: string) {
return fetch(key).then((res) => res.json() as Promise<User | null>);
}
export const useUsers = (id: number | undefined) => {
const { data, error, isLoading } = useSWR(
id ? `https://jsonplaceholder.typicode.com/users/${id}` : null, // idがundefinedならfetchしない
fetcher
);
return {
user: data,
isLoading,
isError: error,
}
}
個人的な不満点
reduxをやめてuseSWRを採用する、となったとき、コンポーネント間で利用したいstateをどう扱うのかを検討する必要がある。useSWRはfetchに関する部分に焦点を当てたhooks(だと感じた)ため、データのfetchを伴わないstate(現在モーダルが開いているかなど)の扱いは、useReducerなどグローバルstateを扱うものを結局のところ導入しないといけない。
ここさえどうにかなればなぁ・・・。
参考