そうです。わたしが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ファースト
- 非同期処理を簡単に扱えるようになる
- 高速で軽量で再利用可能なデータフェッチ
- リクエストの重複排除
- リアクティブな動作の実現
- もちろん、Next.js対応🎊
基本的な使い方
基本的な使い方を見て行きたいと思います👀
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
がundefined
以外だと -> 通信終了
を意味しています。そのため、isLoading
なんてフラグは無いです🐧
error
も同じような感じで、
-
error
がundefined
だと -> エラーが発生していない -
error
がundefined
以外だと -> Promiseがrejectされた(エラーがthrowされた)
となります。
この値を元にコンポーネント内で分岐して表示を切り替えることが出来ますので、サンプルコードでも対応した分岐が書かれています。
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')
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
// When passing a function, SWR will use the return
// value as `key`. If the function throws or returns
// falsy, SWR will know that some dependencies are not
// ready. In this case `user.id` throws when `user`
// isn't loaded.
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}
上記のソースコードでの、/api/projects
へのリクエストは '/api/user'
が完了してからしか実行されません。
また、何らかの要因で '/api/user'
のリクエスト結果(この場合はuser
) が変更した場合は、自動的に /api/projects
へのリクエストが発生します。これにより projects
の値は、常に user
に対してデータの整合性を保つようになっています。
条件付きフェッチの応用ですが、とても有用なテクニックなので、是非ともマスターしておきたいテクニックですね!
パラメータ付きのフェッチ
リクエストにパラメータを付けたい事が多いと思いますが、 useSWR()
にはパラメータを文字列以外にも配列として渡すことが出来ます。サンプルコードを見てみましょう。
const { data: user } = useSWR(['/api/user', token], fetchWithToken)
上記のソースコードでは、useSWR()
の第一引数に配列を渡しています。
これにより、 useSWR()
に token
を認識させることが可能となり、URLにパラメータを含まないようなリクエストにも対応します。
ここでの注意点として、 useSWR()
は配列の内容を浅い比較しかしません。 そのため、引数の値を渡す時には注意を払う必要があります。
// これはやらないで下さい!Depsはレンダリングごとに変更されるようになります!
// そのため通信結果が取得できなくなります!
useSWR(['/api/user', { id }], query)
// 代わりに、「安定した」値のみを渡す必要があります。
useSWR(['/api/user', id], (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は省略できる
因みに、GETリクストかつfetch APIを使うことが出来るのであれば、useSWR()
の第二引数つまり、fetcher
を省略することが出来ます。
※ useSWR()
の第一引数の値は、fetch
関数に渡されるので有効なURL文字列である必要があります。
const { data } = useSWR("/api/user");
※ 後述しますが、optionsでデフォルトの 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 }) // override
// ...
}
function App () {
// SWRConfig以下のコンポーネントにはグローバルな設定が反映される!
return (
<SWRConfig
value={{
refreshInterval: 3000,
// デフォルトのfetcherの挙動を変更できる!
// SWRのデフォルトは (url: string) => window.fetch(url).then(res => res.json())
fetcher: (resource, init) => fetch(resource, init).then(res => res.json())
}}
>
<Dashboard />
</SWRConfig>
)
}
※ ただし useSWR()
にオプションが渡されている場合は、そちらのオプションの方が優先されるので注意が必要です。
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 }) => {
// Never retry on 404.
if (error.status === 404) return
// Never retry for a specific key.
if (key === '/api/user') return
// Only retry up to 10 times.
if (retryCount >= 10) return
// Retry after 5 seconds.
setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000)
}
})
このコールバックにより任意のタイミングでfetcherを再試行できますが、そもそも再試行する必要が無い場合もあると思います。その時は、 shouldRetryOnError: false
を指定する事により再試行を無効にすることが可能です。
Mutation
SWRで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に変更を通知する
};
/* -- 省略 -- */
}
上記のソースコードでは、useSWR()
が返すオブジェクトの中に mutate
と言う関数があります。これに更新したい内容を渡すことで、SWRにキャッシュの更新を通知することが出来ます。
この方法の良い所は、TypeScriptを使っていれば mutate
の引数に型が付いているので、データの整合性が保つ事が出来て扱いやすい事と、refetch(再取得)などの処理を抑えることができる事です。
悪い所を上げるとするならば、useSWR()
を実行する必要があるため、他のコンポーネントとの連携がやりにくいという欠点があります。しかし、それを補う機能がSWRにはありますので、そこまで気にする必要はありません!
方法その2(Refetch Mutation)
Refetch Mutationは、名前が示す通り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"]);
};
/* -- 省略 -- */
}
Bound Mutateと違う所は、mutate関数をインポートから引っ張って来ている事と、実行時にキー文字列( 今回は "/cat" )だけを渡す必要があるという事です。
このように実行する事によって、SWRにrefetch(再取得)を実行するように指示することが出来ます。これによって、サーバーとの間でデータの整合性を保つことが出来ます! サーバーとの同期が多く必要なサービスや、サーバー上で複雑な処理をしている場合は、重宝する方法ですね。
注意点を上げるとすると、関数の名前がBound Mutateと同じ mutate
なので名前の競合が起きやすい事と、多用しすぎるとサーバーに負荷がかかりすぎるので、注意しましょう!👩🏫
ソースコード内のコメントにも書いてありますが、useSWR()の第一引数に配列を渡している場合は、mutate()でも同じ値を持った配列を渡す必要があります。
方法その3(Update Local Mutate)
Update Local Mutateは、Refetch Mutationとほとんど同じような使い方です。
ソースコードを見てみましょう。
import useSWR, { mutate } from "swr";
interface Cat {
id: number;
name: string;
}
const DisplayCatName = () => {
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, { mutate } from "swr";
interface Cat {
id: number;
name: string;
}
const DisplayCatName = () => {
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と同じような使い方ですが、muatate()
の第二引数に非同期関数(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 // reached the end
return `/users?page=${pageIndex}&limit=10` // SWR key
}
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)
ページがフォーカスした時、タブを切り替えた時に、SWRは自動的にデータを再検証します。
これにより、画面の内容を常に最新の状態に保つことが出来るので、サービスのユーザー体験を高めることが出来ます。シナリオとしては、スリープ状態になったPCが復帰した時などに役立つと思います。
具体的な挙動については以下の公式サイトに動画がありますので、是非見て頂けると動作内容を把握できると思います。
また、この機能はデフォルトでは有効になっており、 revaildateOnFocus
オプションで無効にすることが出来ます。
秒間隔で再検証する(Revalidate on Interval)
画面上のデータを時間と共に更新したい時、SWRの refreshInterval
オプションを設定する事で、それが可能になります。
// 1秒ごとに再検証する
useSWR('/api/todos', fetcher, { refreshInterval: 1000 })
これも具体的な処理内容は、以下の公式サイトの動画見て頂けると分かると思います。
注意として、この時に場合によっては通信処理が多く発生する可能性があります!そのため、サーバーへの負担が大きくなることが予想されます。なので、そこまでリアルタイム性を求めていないのであれば、この機能は無効にしてもいいかもしれません。
この機能はデフォルトでは無効となっています。
この機能が有効でも、デフォルトではWebページが画面に表示されていない時、またはネットワーク接続がない時は再検証されません。再検証するには、refreshWhenHidden
または refreshWhenOffline
オプションを有効にする必要があります。
再接続時に再検証する(Revalidate on Reconnect)
ユーザーがオンラインに復帰した時に再検証することも可能です。
このシナリオは、ユーザーのPCがロックを解除した時に頻繁に発生しますが、正直これが本当に必要なモノなのかはサービスによると思いますが、多くのサービスでは必要ないかもしれません。
revalidateOnReconnect
オプションで設定することが出来ます。
useSWR("/api/user", fetcher, { revalidateOnReconnect: true })
※ この機能は navigator.onLine
によってオンラインに復帰したかどうかを判断しています。
この機能はデフォルトで有効になっています。
マウント時に再検証する(Revalidate on Mount)
useSWR()
を実行しているコンポーネントがマウントした時にも、再検証することが可能です。
revalidateOnMount
オプションを設定する事が出来ます。
useSWR("/api/user", fetecher, { revalidateOnMount: true })
デフォルトでは initialData
オプションが設定されていない場合、マウント時に再検証が行われます。
プリフェッチ(Prefetching)
上記で解説した、Mutaionを使う事でデータを予めプリフェッチすることが出来ます。
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を含む)。
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 }
最悪の場合(最初の要求が失敗し、次に再試行が成功した)、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側が値を取得しているかを検知して、最適な描画更新をしているためです。具体的には以下のソースコードです。
※ 公式リポジトリより引用。
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 を利用して値が使われているかを検知して、それぞれを ステートフルな値 として扱っています。
仕組みは意外と簡単なのですが、初見時にちょっと驚いたのは内緒です🤫
この挙動を知らずに、使ってもないのに error
や isValidating
を取得してると、無駄な描画更新が発生してしまうので注意しましょう!
Suspence Mode
Suspence Modeは現在(2021/01時点)、Reactの実験的な機能です。これらのAPIは、Reactの一部になる前に、警告なしに大幅に変更される可能性があります。
詳しくはこちらを参考にして下さい。
ReactSuspenseはSSRモードではまだサポートされていないことに注意してください。
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>
)
}
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との連携
Next.jsでは、SSRとSSGという強力な機能を使うことが出来ます。SWRはそれらの機能と一緒に使うことが出来ます。
具体的には以下のようにします。
export async function getStaticProps() {
// `getStaticProps` is invoked on the server-side,
// so this `fetcher` function will be executed on the server-side.
const posts = await fetcher('/api/posts')
return { props: { posts } }
}
function Posts (props) {
// Here the `fetcher` function will be executed on the client-side.
const { data } = useSWR('/api/posts', fetcher, { initialData: props.posts })
// ...
}
これにより、初期レンダリングを早めつつ、それ以降の動作もSWRによって、動的に素早く動作させることが出来ます。
上記の featcher
は、クライアントとサーバーの両方から実行されるため、両方の環境をサポートする必要があります。
キャッシュについて
SWRにはキャッシュがあるのは既にご存知だと思いますが、そのキャッシュ自体を操作することが出来ます。
今回紹介する機能はテスト用の機能のようです。そのため、それ以外の環境で使う事は想定されていないように感じました。具体的な事は、以下のプルリクを参照してください。
https://github.com/vercel/swr/pull/231
具体的なソースコードは以下のようになります。
import { cache } from "swr";
type keyType = string | any[] | null;
type keyFunction = () => keyType;
type keyInterface = keyFunction | keyType;
// キャッシュをクリア
cache.clear(); // => void;
// キャッシュを削除
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
テスト以外でキャッシュを削除したい場合
もしテスト以外でキャッシュを削除したい場合は、Mutationなどを使って対応しましょう。上記の cache
オブジェクトは、現状テスト以外で使うべきではありません。
オプションについて
useSWR
にはオプションを渡すことが出来ます。
const { data } = useSWR(key, fetcher, optoins);
この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 | リクエストの読み込みに時間がかかりすぎる場合のコールバック関数 |
// 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
を返します。
詳細は以下のプルリクを参照してください🏳🌈
あとがき
ここまでSWRの基本的な使い方から、詳細な挙動について解説してきました。
SWRはとてもシンプルなうえに、複雑な非同期処理を肩代わりしてくれるので、使っていてとても楽しいですね。これからもどんどん使って行きたいと思いました💪
もし記事の内容に何か不備等がございましたら、コメントなどで教えて頂けると幸いです🙏
あっ、あと言い忘れていましたが、記事のタイトルはSWRをリスペクトして名付けました。どうでもいいですね🙄
ここまで読んでくれてありがとうございます。
これが誰かの参考になれば幸いです。
それではまた👋