そうです。わたしがReactをシンプルにするSWRです。
この記事について
SWR について色々と学んだので、その知見をここで共有したいと思います 💪
※ 基本的に以下の公式サイトの情報を参考にしています 📖
そのため、この記事で出すサンプルコードなどは主に上記の公式サイトから引用させてもらっています。予めご了承ください 🙏
SWR とは何か?
SWR は、Next.js を作っているVercel 社が開発しているデータフェッチのための React Hooks ライブラリです。"SWR"と言う名前は、 stale-while-revalidate
の頭文字をとって名付けられています。そのため、SWR はstale-while-revalidate
に基づいた処理と設計になっています。
stale-while-revalidate
について解説したい所ですが、解説するとすごく長くなってしまうため、ここでは「 キャッシュをなるべく最新に保つ機能 」という簡単なまとめ方で留めたいと思います。より知りたい方は RFC か以下のサイトが参考になります 👇
SWR の特徴
- 非常にシンプル
- React Hooks ファースト
- 非同期処理を簡単に扱えるようになる
- 高速で軽量で再利用可能なデータフェッチ
- リクエストの重複排除
- リアクティブな動作の実現
- SSR / SSG に対応
- もちろん、Next.js 対応 🎊
- React Native でも使える 🎐
基本的な使い方
基本的な使い方を見て行きたいと思います 👀
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>
}
上記のソースコードでは、useSWR
というReact Custom Hooksをインポートしてコンポーネント内で使用していますが、少しコードが省略されている部分があるので補足します。
具体的には以下の部分のfetcher
という所です。
const { data, error } = useSWR("/api/user", fetcher);
このfetcher
は、名前から察せられるようにデータを取得する為の関数です。
例えば以下のような関数です。
const fetcher = (url: string): Promise<any> => fetch(url).then(res => res.json());
上記のfetcher
では fetch API を使っていますが、Promise を返す関数であれば別に fetch API を使う必要はありません。Promise に対応させていれば好きなライブラリを使用することが可能です。
SWR の公式サイトでは、いくつかのライブラリの実装例を示してくれています。
次に、useSWR()
が返す値について見てみます。
const { data, error } = useSWR("/api/user", fetcher);
useSWR()
の返り値として、data
・error
をソースコードでは受け取っていますが、data
には fetcher が resolve した値( 通信結果 )、もしくはundefined
が入っています。
これは、
-
data
がundefined
だと -> ロード中 -
data
がPromise.resolve()
した値だと -> 通信終了
を意味しています。そのため、isLoading
なんてフラグは無いです 🐧
error
も同じような感じで、
-
error
がundefined
だと -> エラーが発生していない -
error
が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>
}
条件付きフェッチ
次は、条件付きフェッチについて見て行きましょう。
条件付きフェッチとは文字通り値によって、フェッチするかしないかを判断して処理する方法です。ソースコードを見てみましょう。
// 条件分岐でフェッチ
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
// 関数を渡すこともできる
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
// 第一引数の関数内でエラーを発生させても挙動としては同じようになる
// この時のエラーは、返り値の error で取得できないので注意!
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)
上記のソースコードから分かると思いますが useSWR()
の第一引数にfalsy な値を渡すと、fetcher の実行を停止してくれます。そのため、三項演算子などを使って引数の値を変更しています。
falsy な値については、以下の MDN のドキュメントから確認できますが、JavaScript( TypeScript )はfalsy な値が結構多いです。なので、知らず知らずのうちにfalsy な値を返してしまう事があるので、注意しましょう 📌
また、一番最後の例ですが
// 第一引数の関数内でエラーを発生させても挙動としては同じようになる
// この時のエラーは、返り値の error で取得できないので注意!
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)
コメントにも書いてある通り、引数の関数がエラーを発生させた場合もリクエストの実行をしなくなります。ただこちらは、使うべきではありません!
何故なら、エラーが発生していても useSWR()
はそのエラーを検出してくれません 😢
そのため、仮に第一引数の関数にバグが発生していても気づくのが難しくなってしまいます。なので、できれば第一引数の値には関数は使わないようにした方が良いと思います。
後、以下のように if文
を使った分岐はReact Hooks の規約に違反しているため、書くことができません。注意しましょう!
if( shouldFetch ) {
useSWR('/api/data', fetcher)
}
依存フェッチ
条件付きフェッチを応用して、複数のフェッチを連携させる(依存フェッチともいう)事が可能になります。サンプルコードを見てみましょう。
function MyProjects () {
const { data: user } = useSWR('/api/user', fetcher)
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id, fetcher)
// 関数を渡す場合、SWRは返り値を`key`として使用します。
// 関数がスローまたは falsy な値を返す場合、
// SWRはいくつかの依存関係が準備できてないことを知ることができます。
// この例では、`user.id`は`user`がロードされてない時にスローします。
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}
上記のソースコードでの、/api/projects
へのリクエストは '/api/user'
が完了してからしか実行されません。
また、何らかの要因で '/api/user'
のリクエスト結果(この場合はuser
) が変更した場合は、自動的に /api/projects
へのリクエストが発生します。これにより projects
の値は、常に user
に対してデータの整合性を保つようになっています。
条件付きフェッチの応用ですが、とても有用なテクニックなので、是非ともマスターしておきたいテクニックですね!
因みに、前述した通りuseSWR()
の第一引数には関数を指定するべきではありません!
なので、上記のソースコードは以下のようにした方が良いです 👇
function MyProjects () {
const { data: user } = useSWR('/api/user', fetcher)
- const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id, fetcher)
+ const { data: projects } = useSWR(user ? `/api/projects?uid=${user.id}` : null, fetcher)
// 関数を渡す場合、SWRは返り値を`key`として使用します。
// 関数がスローまたは falsy な値を返す場合、
// SWRはいくつかの依存関係が準備できてないことを知ることができます。
// この例では、`user.id`は`user`がロードされてない時にスローします。
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}
パラメータ付きのフェッチ
リクエストにパラメータを付けたい事が多いと思いますが、 useSWR()
にはパラメータを文字列以外にも配列として渡すことができます。サンプルコードを見てみましょう。
const { data: user } = useSWR(['/api/user', token], fetchWithToken)
上記のソースコードでは、useSWR()
の第一引数に配列を渡しています。
これにより、 useSWR()
に token
を認識させることが可能となり、URL にパラメータを含まないようなリクエストにも対応します。
ここでの注意点として、 useSWR()
は配列の内容を浅い比較しかしません。 そのため、引数の値を渡す時には注意を払う必要があります。
// これはやらないで下さい!Depsはレンダリングごとに変更されるようになります!
// そのため通信結果が取得できなくなります!
useSWR(['/api/user', { id }], fetcher, query)
// 代わりに、「安定した」値のみを渡す必要があります。
useSWR(['/api/user', id], fetcher, (url, id) => query(url, { id }))
また、引数に渡した配列の値は fetcher の引数に渡されます。
const myFetcher = (url: string, id: number) => {
console.log(`url ==> ${url}, id ==> ${id}`);
// ...
};
useSWR(["/api/user", 1], myFetcher); // log出力結果: "url ==> /api/user, id ==> 1"
fetcher は省略できる
後述する<SWRConfig />
のfetcher
オプションを使うことで、デフォルトの fetcher を設定できます。具体的には、以下のようにします 👇
import useSWR, { SWRConfig } from 'swr'
// <SWRConfig />を使って、デフォルトのfetcherを定義する
const App = () => {
return (
<SWRConfig value={{ fetcher: (url) => fetch(url).then(res => res.json()) }}>
<UserList />
</SWRConfig>
)
}
const UserList = () => {
const { data } = useSWR("/api/user"); // fetcherを省略できる!
// ...
}
扱う際には注意が必要ですが、他のライブラリと一緒に使うことで、より簡潔に書けるようになるので、覚えておいて損はないですね!
これで基本的な使い方の紹介は終了です ✨
グローバル設定( Global Configuration )
useSWR()
の第三引数には、オプションを渡すことができます。
useSWR("/api/user", fetcher, options);
options の詳細は下の方に書いてありますので、ぜひ参考にして下さい🙏
しかし、useSWR()
にオプションは渡せても使うたびに渡さないといけないようでは、とても扱いづらくなってしまいます。そこで、<SWRConfig />
を使う事で設定を共通化することができます。
import useSWR, { SWRConfig } from 'swr'
function Dashboard () {
const { data: events } = useSWR('/api/events')
const { data: projects } = useSWR('/api/projects')
const { data: user } = useSWR('/api/user', { refreshInterval: 0 }) // 設定を上書き
// ...
}
function App () {
// SWRConfig以下のコンポーネントにはグローバルな設定が反映される!
return (
<SWRConfig
value={{
refreshInterval: 3000,
fetcher: (resource, init) => fetch(resource, init).then(res => res.json())
}}
>
<Dashboard />
</SWRConfig>
)
}
グローバル設定の取得
useSWRConfig()
を使うことで、グローバル設定を取得することができます。
import { useSWRConfig } from 'swr'
const Component = () => {
// グローバル設定を取得する
const { refreshInterval, mutate, cache, ...restConfig } = useSWRConfig();
/* ... */
}
基本的には、後述する mutate()
や Cache Provider を取得するために使用することが多いと思います。
また、ネストされた設定の場合は拡張(マージ)された設定値が返りますが、 <SWRConfig />
を使っていない場合は、デフォルトの設定値を返します。
Global Error
上記で解説した <SWRConfig />
の onError
オプションにコールバックを設定する事で、エラーをグローバルに処理することができます。
<SWRConfig value={{
onError: (error, key) => {
if (error.status !== 403 && error.status !== 404) {
// We can send the error to Sentry,
// or show a notification UI.
}
}
}}>
<MyApp />
</SWRConfig>
アラート系のライブラリと組み合わせると、シンプルにエラー処理が実装できますので、どんどん活用していきましょう 🤘
Error Retry
SWR では fetcher がエラーを発生した場合、fetcher を 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: retryCount + 1 }), 5000)
}
})
このコールバックにより任意のタイミングで fetcher を再試行できますが、そもそも再試行する必要が無い場合もあると思います。その時は、 shouldRetryOnError: false
を指定する事により再試行を無効にすることが可能です。
Mutation
SWR で、GET 以外の POST や DELETE といった処理には Mutation を使うことで対応できますが、Mutation を使う方法は、筆者が考えるに 4 通りありますので、この節ではその方法とその方法に対する筆者の知見を紹介したいと思います。
※ 方法の名前は分かりやすいさの為に、一部勝手に付けています。ご了承ください。
方法その1( Bound Mutate )
Bound Mutate は、一番オススメの方法です。
先ずは、ソースコードを見てみましょう。
const DisplayCatName = () => {
const { data: cat, mutate } = useSWR("/cat", fetcher);
const onUpdateCatName = async (catName: string) => {
await postCatName(catName); // ネコの名前を更新する非同期関数
mutate({ ...cat, name: catName }); // ここでSWRに変更を通知する
// 再検証されたくない(渡した値を最新とする)場合は、第二引数にfalseを渡してください
// mutate({ ...cat, name: catName }, false);
};
/* -- 省略 -- */
}
上記のソースコードでは、useSWR()
が返すオブジェクトの中に mutate
と言う関数があります。これに更新したい内容を渡すことで、SWR にキャッシュの更新を通知することができます。
この方法の良い所は、TypeScript を使っていれば mutate
の引数に型が付いているので、データの整合性が保つ事ができて扱いやすい事と、処理範囲を限定できる事です。これにより、バグに対処しやすくなります。
悪い所を上げるとするならば、useSWR()
を実行する必要があるため、他のコンポーネントとの連携がやりにくいという欠点があります。しかし、それを補う機能が SWR にはありますので、そこまで気にする必要はありません!
また、mutate()
の第二引数に false
を渡さないと再検証が実行されるため、意図せず refetch(再取得) が発生してしまう事に注意しましょう!
方法その2( Refetch Mutation )
Refetch Mutation は、名前が示す通り refetch(再取得) によってデータの整合性を保つ方法です。ソースコードを見てみましょう。
import useSWR, { useSWRConfig } from "swr";
const DisplayCatName = () => {
const { mutate } = useSWRConfig();
const { data: cat } = useSWR("/cat", fetcher);
const onUpdateCatName = async (catName: string) => {
await postCatName(catName); // ネコの名前を更新する非同期関数
mutate("/cat"); // ここでSWRに変更を通知するが、文字列だけ渡している事に注意!
// もしキーを配列で渡している場合は以下のようにします
// mutate(["/cat"]);
};
/* -- 省略 -- */
}
Bound Mutate と違う所は、 mutate
関数を useSWRConfig()
から取得していることと、実行時にキー文字列( 今回は "/cat" )だけを渡す必要があるという事です。
このように実行する事によって、SWR に refetch(再取得) を実行するように指示することができます。これによって、サーバーとの間でデータの整合性を保つことができます! サーバーとの同期が多く必要なサービスや、サーバー上で複雑な処理をしている場合は、重宝する方法ですね。
注意点を上げるとすると、関数の名前が Bound Mutate と同じ mutate
なので名前の競合が起きやすい事と、多用しすぎるとサーバーに負荷がかかりすぎるので、注意しましょう!👩🏫
方法その3( Update Local Mutate )
Update Local Mutate は、Refetch Mutation とほとんど同じような使い方です。
ソースコードを見てみましょう。
import useSWR, { useSWRConfig } from "swr";
interface Cat {
id: number;
name: string;
}
const DisplayCatName = () => {
const { mutate } = useSWRConfig();
const { data: cat } = useSWR<Cat>("/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));
};
/* -- 省略 -- */
}
実行方法は Refetch Mutation とほとんど同じですが、mutate()
の第二引数に更新する値を渡している所が違います。
このやり方が便利な所は、refetch(再取得) が発生しない事と、別の場所で使われている useSWR()
に変更を通知することができます。これによって、離れた位置のコンポーネントの状態などを変更することができるので、うまく使えばサーバーへの負荷を抑えつつキャッシュをより有効活用することができます 💪
ただ注意としては、使いすぎると複雑なバグを引き起こしかねないので、Bound Mutate が使える場合は、そちらを使った方が良いと思います 🤔 ※ Boud Muatate だと範囲を限定できるので、バグを限定的にすることができます。
方法その4( Mutate Based on Current Data )
最後に Mutate Based on Current Data を紹介します。
ソースコードを見てみましょう。
import useSWR, { useSWRConfig } from "swr";
interface Cat {
id: number;
name: string;
}
const DisplayCatName = () => {
const { mutate } = useSWRConfig();
const onUpdateCatName = async (catName: string) => {
mutate("/cat", async (cat: Cat): Promise<Cat> => {
const newCat = { ...cat, name: catName };
await postCat(newCat); // ネコの情報を更新する非同期関数
return newCat; // ここで新しい値を返す必要がある!
});
};
/* -- 省略 -- */
}
こちらも Refetch Mutation や Update Local Mutate と同じような使い方ですが、mutate()
の第二引数に非同期関数( async function )を渡している点が違います。引数に渡した非同期関数は、 useSWR()
が持っているキャッシュを受け取り、次のキャッシュの値を返します。
この方法は、現在取得しているデータを用いた変更処理を行う時に大変便利な方法です。これによって、わざわざ useSWR()
を実行して値を取得する必要がありませんので、コンポーネントの描画を抑えることができますし、他の方法ではできない複雑な処理を実行する事も可能となっています。
ただ、こちらの方法はデータの整合性を保つのが難しい(引数で受け取る値が any)ですし、複雑なロジックが実行できるという事は、それだけバグを発生させやすいという事でもありますので、多用は禁物だと思います 🦉
Mutation まとめ
上記の方法を筆者の知見を交えてまとめたいと思います。
方法名 | メリット | デメリット | オススメ度 |
---|---|---|---|
Bound Mutate | 簡単にデータを更新でき、範囲を限定的にできる | 複雑な処理はできない、 useSWR 必須 | 高 |
Refetch Mutation | データの整合性を高めることができる | 非同期処理が走るため処理が遅くなるかも | 中 |
Update Local Mutate | useSWR を使わなくてもキャッシュを更新できる | Bound Mutate で代用できることが多い | 低 |
Mutate Based on Current Data | 複雑な処理が可能 | バグを引き起こしやすい | 中 |
ページネーション( Pagination )
次はページネーション機能について見てきたいと思います。
先ずはサンプルコードを見てみましょう。
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>
}
ページネーション機能とはいっても、 useState()
でページのインデックスを管理し、それを useSWR()
のパラメータとして使っているだけです。基本的な使い方の応用ですね。
useSWR()
は、一度取得したデータをキャシュとして保持するので、一回取得してしまえば後は高速に動作します。これがとてもシンプルな実装でできるのはとても良いですよね ✨
しかし、無限ローディングなどの終わりのないページネーションや取得したデータ全体を扱いたい場合は、上記の方法では少し面倒な実装になってしまいます。そこで、SWR 側が useSWRInfinite()
という Hooks を用意してくれています!やったね!
useSWRInfinite()
を使う事で、簡単に無限ローディングなど実装することができます。
// 各ページのSWRキーを取得する関数
// 返り値は `fetcher` に渡されます
// `null` が返された場合、そのページのリクエストは開始されません。
const getKey = (pageIndex: number, previousPageData: any[]) => {
if (previousPageData && !previousPageData.length) return null // 最後のページに到達した
return `/users?page=${pageIndex}&limit=10` // fetcherの第一引数に渡される値
// 配列を渡すこともできます。
// 配列の場合は、fetcher(...[`/users`, pageIndex, 10])として展開されます
// return [`/users`, pageIndex, 10]
}
function App () {
const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
if (!data) return 'loading'
// 取得した全データを使って計算できます
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`は、各ページのAPIレスポンスの配列です。
return users.map(user => <div key={user.id}>{user.name}</div>)
})}
<button onClick={() => setSize(size + 1)}>Load More</button>
</div>
)
}
getKey()
は、useSWR()
との大きな違いです。現在のページのインデックスと前のページデータを受け取ります。そのため、インデックスベースとカーソルベースの両方のページネーション API を適切にサポートできます。
上記のソースコードが受け取る値(fetcher が返す値)は、以下のようになっている事に注意してください。
GET '/users?page=0&limit=10'
[
{ name: 'Alice', ... },
{ name: 'Bob', ... },
{ name: 'Cathy', ... },
...
]
そのため useSWRInfinite()
の data
の結果は、以下のようになっています。
// `data` will look like this
[
[
{ name: 'Alice', ... },
{ name: 'Bob', ... },
{ name: 'Cathy', ... },
...
],
[
{ name: 'John', ... },
{ name: 'Paul', ... },
{ name: 'George', ... },
...
],
...
]
これでページネーション機能の紹介は終わりです。シンプル過ぎて、特に解説するところもなかったですね 😅
自動再検証( Auto Revalidation )
自動再検証( Auto Revalidation )について解説したいと思います。
SWR と言う名前は、stale-while-revalidate の頭文字から来ていると言いましたが、この stale-while-revalidate にはキャッシュを更新する必要があるのかを検証するプロセスがあります。勿論、SWR も stale-while-revalidate を踏まえた設計になっているので、検証をするのですが、SWR では自動で検証するようになっており、その検証タイミングも様々なモノがあります。
この節では、どのタイミングで自動再検証が行われているのかを見て行きたいと思います。
フォーカス時に再検証する( Revalidate on Focus )
ページがフォーカスした時、タブを切り替えた時、またはfocusThrottleInterval
オプションで指定した期間内( ポーリングする時間 )に、SWR は自動的にデータを再検証します。
これにより、画面の内容を常に最新の状態に保つことができるので、サービスのユーザー体験を高めることができます。シナリオとしては、スリープ状態になった PC が復帰した時などに役立つと思います。
具体的な挙動については以下の公式サイトに動画がありますので、是非見て頂けると動作内容を把握できると思います。
秒間隔で再検証する( Revalidate on Interval )
画面上のデータを時間と共に更新したい時、SWR の refreshInterval
オプションを設定する事で、それが可能になります。
// 1秒ごとに再検証する
useSWR('/api/todos', fetcher, { refreshInterval: 1000 })
これも具体的な処理内容は、以下の公式サイトの動画見て頂けると分かると思います。
注意として、この時に場合によっては通信処理が多く発生する可能性があります!そのため、サーバーへの負担が大きくなることが予想されます。なので、そこまでリアルタイム性を求めていないのであれば、この機能は無効にしてもいいかもしれません。
再接続時に再検証する( Revalidate on Reconnect )
ユーザーがオンラインに復帰した時に再検証することも可能です。
このシナリオは、ユーザーの PC がロックを解除した時に頻繁に発生しますが、正直これが本当に必要なモノなのかはサービスによると思いますが、多くのサービスでは必要ないかもしれません。
revalidateOnReconnect
オプションで設定することができます。
useSWR("/api/user", fetcher, { revalidateOnReconnect: true })
※ この機能は navigator.onLine
によってオンラインに復帰したかどうかを判断しています。
マウント時に再検証する( Revalidate on Mount )
useSWR()
を実行しているコンポーネントがマウントした時にも、再検証することが可能です。
revalidateOnMount
オプションを設定する事ができます。
useSWR("/api/user", fetecher, { revalidateOnMount: true });
自動再検証を行いたくない場合
プロジェクトによっては、上記すべての再検証を行いたくない場合があります。
そのような場合には、 useSWRImmutable()
を使うことで、すべての自動再検証を無効にできます 👇
import useSWRImmutable from 'swr/immutable'
useSWRImmutable(key, fetcher, options)
// useSWRImmutable()は、以下と同等です。
useSWR(key, fetcher, {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false
})
基本的には、useSWR()
と同じオプションや挙動をするので、useSWR()
の糖衣構文として使えます。
プリフェッチ(Prefetching)
上記で解説した、Mutation を使う事でデータを予めプリフェッチすることができます。
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 でやる事らしいので、こちらが使えるなら、Top-Level の方を使った方が良いと思います。以下は公式サイトからの引用&要約です。
SWR のデータをプリフェッチする方法はたくさんあります。トップレベルのリクエストに rel="preload"を指定する方法は、強くお勧めします。
<link rel="preload" href="/api/data" as="fetch" crossorigin="anonymous" />
HTML の<head>内に配置するだけです。簡単、高速、ネイティブです。
JavaScript がダウンロードを開始する前であっても、HTML がロードされるときにデータをプリフェッチします。 同じ URL を使用するすべての着信フェッチ要求は、結果を再利用します(もちろん、SWR を含む)。
初期値を設定する
SSR や SSG を使用している時などでは、useSWR()
でリクエストする前に既にデータを取得していることがあります。そのような場合には、fallbackData
オプションを使うことで、あらかじめ初期値を設定することができます。
// "hoge"を`'/api/data'`の初期値として設定する
useSWR('/api/data', fetcher, { fallbackData: "hoge" })
また、<SWRConfig />
のfallback
オプションを使うことで、より広範囲かつ柔軟に初期値を設定できます 👇
<SWRConfig value={{
fallback: {
'/api/user': { name: 'Bob', ... },
'/api/items': {...},
...
}
}}>
<App/>
</SWRConfig>
Dependency Collection について( 重要 )
Dependency Collection について解説したいと思いますが、これは結構重要です。
なので、SWR を使う人は是非とも知っておきたい知識となっていますので、気合入れて読んでください 🥋
まず、公式サイトの内容を引用&要約を以下に記述します。
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 }
最悪の場合(最初の要求が失敗し、次に再試行が成功した)、4 行のログが表示されます。
console.logの結果// console.log(data, error, isValidating) undefined undefined true // => フェッチの開始 undefined Error false // => フェッチの完了、エラーを取得 undefined Error true // => 再試行の開始 Data undefined false // => 再試行の完了、データを取得
この状態変化は理にかなっています。しかし、それはまた、コンポーネントが 4 回レンダリングされることを意味しています。
コンポーネントを変更して
data
だけを使用する場合:dataだけ取得するように修正(fetcherの挙動は変化してない)function App () { const { data } = useSWR('/api', fetcher) console.log(data) return null }
魔法が起こります — 今回は二つの再レンダリングしかありません:
console.logの結果// console.log(data) undefined // => 再利用 / 初期レンダリング Data // => 再試行の完了、データを取得
まったく同じプロセスが内部で発生し、最初のリクエストからエラーが発生し、再試行からデータを取得しました。ただし、SWR はコンポーネントによって使用されている状態のみを更新します。 今回の例の場合は、
data
のみ。
上記の内容では、useSWR()
から受け取る値を変更するだけで、コンポーネントの描画更新が変化しています。
なぜこのような事が起こるのかと言うと、SWR 側が値を取得しているかを検知して、最適な描画更新をしているためです。具体的には以下のソースコードです。
※ 公式リポジトリのソースコードより引用。
return {
mutate: boundMutate,
get data() {
stateDependencies.data = true
return data
},
get error() {
stateDependencies.error = true
return error
},
get isValidating() {
stateDependencies.isValidating = true
return isValidating
}
} as SWRResponse<Data, Error>
上記のソースコードは、getter を利用して、値が使われているかを検知して、それぞれを ステートフルな値 として扱っています。
仕組みは意外と簡単なのですが、初見時にちょっと驚いたのは内緒です 🤫
この挙動を知らずに、使ってもないのに error
や isValidating
を取得してると、無駄な描画更新が発生してしまうので注意しましょう!
Suspence Mode
suspence
オプションを有効にすることで Suspence Mode を有効にすることができます。
import { Suspense } from 'react'
import useSWR from 'swr'
function Profile () {
const { data } = useSWR('/api/user', fetcher, { suspense: true }) // suspenceを有効に設定!
return <div>hello, {data.name}</div>
}
function App () {
return (
<Suspense fallback={<div>loading...</div>}>
<Profile/>
</Suspense>
)
}
Suspence Mode では、useSWR()
が返す data
は、常に通信結果になります(undefined
は含まれない)。正し、エラーが発生した場合は、Error Boundary を使用してエラーをキャッチする必要があります。
<ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}>
<Suspense fallback={<h1>Loading posts...</h1>}>
<Profile />
</Suspense>
</ErrorBoundary>
Note:条件付きフェッチを使用する場合
通常、suspence
オプションを有効にすると、レンダリング時に data
が準備されている事が保証されています。
しかし、条件付きフェッチ又は依存フェッチと一緒に使用すると、要求が一時停止された場合に data
が undefined
になります。
function Profile () {
const { data } = useSWR(isReady ? '/api/user' : null, fetcher, { suspense: true })
// `data` will be `undefined` if `isReady` is false
// ...
}
この制限に関する技術的な詳細が知りたい方は、こちらのDiscussionを参照してください。
Next.js との連携( SSG/SSR 対応 )
Next.js では、SSG と SSR という強力な機能を使うことができます。SWR はそれらの機能と一緒に使うことができます。
具体的には以下のようにします 👇
export async function getStaticProps () {
// `getStaticProps` はサーバー側で実行されます
const article = await getArticleFromAPI()
return {
props: {
fallback: {
'/api/article': article // '/api/article' の初期値を返す
}
}
}
}
export default function Page({ fallback }) {
// getStaticProps()で受け取った初期値を反映する
return (
<SWRConfig value={{ fallback }}>
<Article />
</SWRConfig>
)
}
function Article() {
// `data` の中には <SWRConfig /> で設定された初期値が渡される
const { data } = useSWR('/api/article', fetcher)
return <h1>{data.title}</h1>
}
これにより、初期レンダリングを早めつつ、それ以降の動作も SWR によって、動的に素早く動作させることができます。
また、個別に初期値を設定したい場合は、fallbackData
を使うと便利です 👇
useSWR('/api/article', fetcher, { fallbackData: articleValue })
Middleware について
SWR では、デフォルトの挙動を拡張できる Middleware を設定できます。
具体的には以下のような関数です 👇
function myMiddleware (useSWRNext) {
return (key, fetcher, config) => {
// フックが実行される前...
// 次のミドルウェア、またはこれが最後のミドルウェアの場合は `useSWR` を処理します。
const swr = useSWRNext(key, fetcher, config)
// フックが実行された後...
return swr
}
}
上記の関数を use
オプションに渡すことで、useSWR()
の挙動を拡張できます 👇
<SWRConfig value={{ use: [myMiddleware] }}>
{/* ... */}
</SWRConfig>
// または...
useSWR(key, fetcher, { use: [myMiddleware] })
注意点として、 Middleware は実行順に継承されていくため、以下の二つの処理は同じになります 👇
function Bar () {
useSWR(key, fetcher, { use: [c] })
// ...
}
function Foo() {
return (
<SWRConfig value={{ use: [a] }}>
<SWRConfig value={{ use: [b] }}>
<Bar/>
</SWRConfig>
</SWRConfig>
)
}
useSWR(key, fetcher, { use: [a, b, c] })
実行順について
Middleware の理解を深めるために、実際に Middleware の実行順を確認していきましょう。
まずは、以下のような処理を書いたとします 👇
useSWR(key, fetcher, { use: [a, b, c] })
この時の実行順は、以下のようになります 👇
enter a
enter b
enter c
useSWR()
exit c
exit b
exit a
上記から分かるように、a → b → c → useSWR()
の順で実行されています。
これはつまり、もし a
の部分にバグがあり処理が中断した場合は、それ以降の b → c → useSWR()
は実行されないことを意味します。そのため、Middleware の順番は重要であり、Middleware のバグは大きなバグに繋がりやすいことを理解しておく必要があります。
具体例について
一番身近な具体例として、上記で解説した useSWRInfinite()
が挙げられます 👇
また、公式ドキュメントには他の Middleware の実装例も紹介されていますので、Middleware を自作する際の参考にするといいと思います 👇
キャッシュについて
SWR では、Cache Provider を使用してキャッシュの処理をカスタマイズすることができますが、Cache Provider として使用するには、以下の型を満たすようなオブジェクトである必要があります 👇
interface Cache<Data> {
get(key: string): Data | undefined
set(key: string, value: Data): void
delete(key: string): void
}
※ 上記の型を満たすものとして JavaScript の Map などがあります。
Cache Provider の設定
Cache Provider を設定するには、<SWRConfig />
を使用します。
import useSWR, { SWRConfig } from 'swr'
function App() {
return (
<SWRConfig value={{ provider: () => new Map() }}>
<Page/>
</SWRConfig>
)
}
またこの時、 provider
に渡す関数の第一引数には、親のコンポーネントの Cache Provider ( 無い場合はデフォルトのキャッシュ )を受け取る事ができるため、それを使って拡張する事もできます 👇
<SWRConfig value={{ provider: (cache) => newCache }}>
...
</SWRConfig>
Cache Provider の取得
カスタマイズした Cache Provider は、useSWRConfig()
で取得することができます 👇
import { useSWRConfig } from 'swr'
const Component = () => {
const { cache } = useSWRConfig()
const value = cache.get("/api/users") // "/api/users"のキャッシュを取得する
const onClear = () => cache.clear() // すべてのキャッシュを削除
// ...
}
具体的な処理について
公式サイトにいくつかの具体例がありますので、そちらを参考にして頂けたらと思います 👇
React Native への対応
SWR は、React Native でも使用することができます。
基本的な使い方は同じですが、focus
や online
といったブラウザ特有のイベントを React Native 用に設定しなおす必要があります👇
<SWRConfig
value={{
provider: () => new Map(),
isOnline() {
/* ネットワーク状態の検出器をカスタマイズ */
return true
},
isVisible() {
/* 視認状態の検出器をカスタマイズ */
return true
},
initFocus(callback) {
/* リスナーをステートプロバイダーに登録 */
},
initReconnect(callback) {
/* リスナーをステートプロバイダーに登録 */
}
}}
>
<App />
</SWRConfig>
isOnline
と isVisible
は、アプリが "アクティブ" かどうかを判断するための関数です。デフォルトでは、上記二つが "アクティブ" でない場合、 SWR は再検証を中止します。
また、 initFocus
と initReconnect
を使用するには、前述した Cache Provider も設定する必要があります。
上記の設定について、initFocus
の実装については、公式ドキュメントに具体例がありましたので、以下にソースコードを引用します 👇
<SWRConfig
value={{
provider: () => new Map(),
isVisible: () => { return true },
initFocus(callback) {
let appState = AppState.currentState
const onAppStateChange = (nextAppState) => {
/* バックグラウンドモードまたは非アクティブモードからアクティブモードに再開する場合 */
if (appState.match(/inactive|background/) && nextAppState === 'active') {
callback()
}
appState = nextAppState
}
// アプリの状態変更を監視する
const subscription = AppState.addEventListener('change', onAppStateChange)
return () => {
subscription.remove()
}
}
}}
>
<App>
</SWRConfig>
上記のソースコードにより、React Native でもアクティブモードへ移行した時に、フォーカス時の再検証( Revalidate on Focus )が実行されるようになります。
オプションについて
useSWR
にはオプションを渡すことができます。
const { data } = useSWR(key, fetcher, optoins);
このoptions
の内容をご紹介したいと思いますが、結構量があるため、簡単な解説と型情報をのみを記述したいと思います。
オプション一覧
suspense
型 | デフォルト値 | 効果 |
---|---|---|
boolean | false | React Suspence モードを有効にします。 |
fetcher
型 | デフォルト値 | 効果 |
---|---|---|
※以下参照 | undefined | デフォルトのfetcher 関数を設定します |
type Fetcher<Data> = (...args: any) => Data | Promise<Data>
fallback
型 | デフォルト値 | 効果 |
---|---|---|
any | undefined | 初期データの key-value オブジェクト |
fallbackData
型 | デフォルト値 | 効果 |
---|---|---|
any | undefined | 初期値を設定します |
revalidateIfStale
型 | デフォルト値 | 効果 |
---|---|---|
boolean | true | 古いデータがある場合でも、マウント時に自動再検証をする |
revalidateOnMount
型 | デフォルト値 | 効果 |
---|---|---|
boolean | ※以下参照 | コンポーネントがマウントした時に自動的に再検証する |
※ デフォルトでは fallbackData
が設定されてない場合、マウント時に再検証されます。false
だと fallbackData
を設定していても、再検証されません。
revalidateOnFocus
型 | デフォルト値 | 効果 |
---|---|---|
boolean | true | ウィンドウがフォーカスされたときに自動的に再検証する |
※ focusThrottleInterval
オプションで検証する期間を変更することができます。
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 | 再検証する期間を指定する |
※ ここでの再検証とは、revalidateOnFocus
の事を指します。revalidateOnFocus
オプションが false の場合は、このオプションは機能しません。
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
を返します。
詳細は以下のプルリクを参照してください 🏳🌈
use
型 | デフォルト値 | 効果 |
---|---|---|
※以下参照 | undefined | ミドルウェア関数の配列 |
type Middleware = (useSWRNext: SWRHook) => SWRHookWithMiddleware
type use = Middleware[]
あとがき
ここまでSWRの基本的な使い方から、詳細な挙動について解説してきました。
SWRはとてもシンプルなうえに、複雑な非同期処理を肩代わりしてくれるので、使っていてとても楽しいですね。これからもどんどん使って行きたいと思いました 💪
もし記事の内容に何か不備等がございましたら、コメントなどで教えて頂けると幸いです 🙏
あっ、あと言い忘れていましたが、記事のタイトルはSWRをリスペクトして名付けました。どうでもいいですね 🙄
ここまで読んでくれてありがとうございます。
これが誰かの参考になれば幸いです。
それではまた 👋
Discussion
ちょっと言い忘れていたので、コメントで追記。
SWRのソースコードは少ない&読み易いので、OSS入門としてオヌヌメです👍
貢献には興味なくても、ソースコードを見るだけでも良い刺激になると思いますよ🙃