はじめに
SWRとはReactのデータ取得を快適に行うためのAPIを提供するライブラリです。
2022/12/09にメジャーバージョンである2.0がリリースされました。それを記念してこの記事では2.0によって追加された新機能を紹介します。
SWRが提供する移行ガイドはこちらです。
useSWRMutation
SWR2.0ではuseSWRMutation
という新しいhooksを利用できるようになりました。useSWR
のようなhooksが呼び出されたタイミングで発火するのではなく任意のタイミングで発火させることができます。useSWRMutation
は以下のように書いて利用します。
import useSWRMutation from 'swr/mutation'
const { data, error, trigger, reset, isMutating } =
useSWRMutation(key, fetcher, options);
返り値も引数も様々ありますね。実際にサンプルを作って動作を確認していきます。
基本
例としてToDoリストを扱うアプリを考えます。一覧はuseSWR
を用いてこのように取得します。
const { tasks } = useSWR('/api/tasks', getTasks);
これまではタスクを追加する処理のような更新処理を扱うときはmutate
を用いて
const [text, setText] = useState('');
const [isMutating, setIsMutating] = useState(false);
return (
<>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<button
type="button"
onClick={() => {
setIsMutating(true);
mutate('/api/tasks', postTask(text)).finally(() => {
setIsMutating(false);
});
}}
disabled={isMutating}
>
追加
</button>
</>
);
と書いていました。useSWRMutation`を用いると下のように書くことができます。
const { trigger, isMutation } = useSWR('/api/tasks', postTask);
const [text, setText] = useState('');
return (
<>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<button type="button" onClick={() => trigger(text)} disabled={isMutating}>追加</button>
</>
);
更新中の状態を表すisMutation
を取得できるようになったことなど、hooksにしたことでより宣言的(React like)な書き方になり見通しが非常に良くなりました。
mutation
とuseSWRMutation
どちらのkey
もタスク一覧を取得するAPIと同様のkey
を与えたので、更新の完了と同時にタスク一覧を読み直してくれます。ミューテーションを起こす関数のkey
と同名のkey
をもつものが再読み込みしてくれるわけです。
楽観的UI更新
先ほど説明した更新後のデータの読み直しですが、更新処理とデータの読み込みを待つ必要があるのでアプリを利用しているユーザーは更新後しばらく古い状態のまま利用しなければいけないです。これはユーザー体験として優れていません。
SWRでは更新処理とデータの読み込みが終わるまでの代わりのデータを与えることでUI上では新しい状態が表示されるような機能の提供を始めました。この機能はuseSWRMutation
だけでなくmutate
でも利用できます。
この機能は本来実装が大変ですが、SWRで提供される機能では以下のように簡単に実装することができます。
const { trigger, isMutation } = useSWR('/api/tasks', postTask);
const [text, setText] = useState('');
return (
<>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<button
type="button"
onClick={() => (
trigger(
text,
{
optimisticData: (tasks) => [...tasks, newTask(text)],
}
)
disabled={isMutating}
}>
追加
</button>
</>
);
newTask
は新しいタスクを作る関数です。trigger
関数の第二引数であるoptions
のoptimisticData
に更新前のデータを受け取って更新した結果を返す関数を置くことでisMutation
の間はその仮のデータを表示するようになります。大変簡単で便利なので助かります。mutate
の場合も同じように第三引数であるoptions
のoptimisticData
で設定することができます。
このような手法を用いた場合は更新処理が失敗した時に元の状態に戻す必要があります。options
のrollbackOnError
をtrue
に設定すると定義した更新処理がrejectされたときに自動で元の状態に戻ります。
他にもpopulateCache
を設定することで更新処理の結果をミューテーション予定のデータとすることができます。これによってデータ取得APIの読み込みを防ぐことができます(このとき再検証を防ぐためにrevalidate
はfalseにする必要があります)。
複数キーのミューテーション
これまではmutate
のkey
にuseSWR
のkey
と同じ引数しか渡せなかったので、ミューテーションされるキーは一つでした。SWR2.0ではフィルタ関数と呼ばれる関数を渡すことで複数のキーをミューテーションすることができます。tasks
を含む全てのキーを更新する場合は以下のように書きます。
mutate((key: string) => /^(?=.*task).*$/.test(key), updateTask);
これを利用して
mutate(() => true, undefined, { revalidate: false });
のようにすれば全てのキーをリセットすることも可能です。
これらを利用したサンプル
どのAPIも0.5秒後に実行されるようにしたサンプルを作成しました。追加や更新も0.5秒かかりますが、楽観的UI更新によってそれを感じさせません。trigger
を行うだけでデータの読み込みも自動でできていることもuseSWRMutation
があってこそです。
このデモでInvalid or unexpected token browser
こんな感じのエラーが出てる場合は×を押して使ってください。これが出る原因がわからないのですが、ローカル環境では問題なく動くのでcodesandbox起因じゃないかと考えています。原因を知っている方いれば教えてください。
preload
preload
を使うことで事前にデータを読み込むことができます。
preload('/api/tasks', getTasks);
Reactの外、グローバルスコープで呼ぶこともできますのでコンポーネントが呼ばれる段階にはデータの準備を完了させて読み込みの隙をなくすことができます(完了していなくても途中までは読み込めているので素早く表示できます)。
Reactの中でも遷移先が決まっているときは先に読み込んであげたり、決まっていなくてもホバーやフォーカスなどのタイミングで読み込んであげたりすることで遷移後の読み込み時間が削減され良いユーザー体験を提供することができます。
preload
によって読み込みを行なっているときに、同キーをuseSWR
などで読み込む場合はpreload
の読み込みを待ってそれによる結果を利用します。prelaod
の呼び出しが棄却されるわけではないので安心して使えます。
isLoading
useSWR
の返り値に新しくisLoading
が追加されました。初期データを読み込んでいるの状態の時にのみtrue
になる値です。
これまではisValidating
を利用してisValidating
がtrue
の時にデータとエラーが存在しない時を定義する必要がありました。
const { data, error, isValidating } = useSWR('/api/tasks', getTasks);
const isLoading = !(data && error) && isValidating;
記述がめんどくさかったので助かりますね。isLoading
が台頭したことでisValidating
が不要になるわけではありません。初期ローディング状態とデータを持っているときのローディング状態で出し分けが可能だからです。例えば初期ではコンポーネント全体にスピナーを出すようにして、それ以外はデータを持っているのでデータの横にスピナーを小さく出すようなことができます。
keepPreviousData
新しく追加されたオプションです。key
が変化するような読み込みを行うときに、key
ごとの結果を保持しておくような設定を行うことができます。検索をonChange
のタイミングで行う時などに有効です。タスクを検索する例を見てみます。
const [text, setText] = useState('');
const { data: task } = useSWR(
`/api/tasks?name=${text}`,
getTask,
{
keepPreviousData: true
}
);
return (
<>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<Task task={task} />
</>
);
この例だとhello world
と検索したい時にh
で/api/tasks?name=h
、he
で/api/tasks?name=he
と一文字ずつ取得します。そしてそれぞれの結果を保存して持っています。hello world
と検索した後にhello
の結果を見ようとするとhello world
を見る過程であらかじめ取得しているのですぐにデータを見ることができます。
SWRConfigの継承
SWRConfigを親でも設定している場合それを引き継ぐことが可能になりました。
<SWRConfig
value={parentConfig => ({
...parentConfig,
suspense: true,
})}
>
{children}
</SWRConfig>
これまでテスト環境などで同じ設定が使えるように別ファイルにconfig
を設定していたのですが、この機能によってその必要は無くなりました。
さいごに
メジャーバージョンアップなだけはあってかなり有用な機能が多かった印象です。mutate周りはもう少しだけ機能が欲しいなあと思っていたのでたくさん追加されてとても満足です。他にも変更や開発者ツールの紹介があったので確認すると面白いと思います。