23
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

1人フロントエンドAdvent Calendar 2022

Day 16

SWRの2.0がリリースされていたので新機能を学ぶ

Last updated at Posted at 2022-12-15

はじめに

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)な書き方になり見通しが非常に良くなりました。
mutationuseSWRMutationどちらの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関数の第二引数であるoptionsoptimisticDataに更新前のデータを受け取って更新した結果を返す関数を置くことでisMutationの間はその仮のデータを表示するようになります。大変簡単で便利なので助かります。mutateの場合も同じように第三引数であるoptionsoptimisticDataで設定することができます。
このような手法を用いた場合は更新処理が失敗した時に元の状態に戻す必要があります。optionsrollbackOnErrortrueに設定すると定義した更新処理がrejectされたときに自動で元の状態に戻ります。
他にもpopulateCacheを設定することで更新処理の結果をミューテーション予定のデータとすることができます。これによってデータ取得APIの読み込みを防ぐことができます(このとき再検証を防ぐためにrevalidateはfalseにする必要があります)。

複数キーのミューテーション

これまではmutatekeyuseSWRkeyと同じ引数しか渡せなかったので、ミューテーションされるキーは一つでした。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を利用してisValidatingtrueの時にデータとエラーが存在しない時を定義する必要がありました。

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=hhe/api/tasks?name=heと一文字ずつ取得します。そしてそれぞれの結果を保存して持っています。hello worldと検索した後にhelloの結果を見ようとするとhello worldを見る過程であらかじめ取得しているのですぐにデータを見ることができます。

SWRConfigの継承

SWRConfigを親でも設定している場合それを引き継ぐことが可能になりました。

<SWRConfig
  value={parentConfig => ({
    ...parentConfig,
    suspense: true,
  })}
>
  {children}
</SWRConfig>

これまでテスト環境などで同じ設定が使えるように別ファイルにconfigを設定していたのですが、この機能によってその必要は無くなりました。

さいごに

メジャーバージョンアップなだけはあってかなり有用な機能が多かった印象です。mutate周りはもう少しだけ機能が欲しいなあと思っていたのでたくさん追加されてとても満足です。他にも変更や開発者ツールの紹介があったので確認すると面白いと思います。

23
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?