<-- mermaid -->

SWRで再検証を行うタイミングを制御する

はじめまして! AlphaDriveでエンジニアをしている、神と申します!

今回は、SWRで再検証を行うタイミングを制御する方法についてご紹介したいと思います!

この記事で伝えたいこと

  • SWRは、キャッシュを削除することで再検証を行うタイミングを制御できるということ
  • 関連する複数のkeyのキャッシュを同時に削除する方法

背景

弊チームではRedux + Redux Sagaを使用していましたが、リアーキテクチャの一環としてSWRへの置き換えを進めています。 そういった中で、データを一覧表示するリスト画面のデータ取得をSWRで行うように変更することになりました。

データを一覧表示するリスト画面の仕様は、下記のようになります

  1. 複数の画面でデータの作成、更新、削除ができ、各画面でデータの再取得を適切に行わないといけません。
  2. ページネーション機能と表示件数を制限する機能もあるので、それらも考慮しなければなりません。

実装してみる

データを一覧表示するリスト画面を実装してみようと思います。
仕様で上げた

ページネーション機能と表示件数を制限する機能もあるので、それらも考慮しなければなりません。

については、SWRのkeyにpageやlimit (state)を含ませれば良さそうです。SWRのkeyにpageやlimitを含んでおくことで、ページ数や表示件数の条件に応じて適切にキャッシュが保存されます。ユーザーの動き(ページ番号のクリックなど)に合わせてこれらを変更することによってリクエストを行います。

https://swr.vercel.app/ja/docs/pagination

例:

リストのコンポーネント

export const List = () => {
    const fetcher = async (url: string) => await api.get(url)
    const [page, setPage] = useState(1)
    const [limit setLimit] = useState(10)
    const { data, mutate } = useSWR(`data?page=${page}&limit=${limit}`, fetcher)

    return (
        // 省略
    )
}

作成、編集、削除などのボタンコンポーネント

export const Button ({ mutateKey }: ButtonProps) => {
    const { mutate } = useSWRConfig()

    const onClick = () => {
        //省略 データ作成、編集、削除処理

        mutate(mutateKey)
    }

    return (
        <button
            onClick={onClick}
        >
            ボタン
        </button>
    )
}

詰まった箇所

結論を言うと、上記コードでは要件は満たせませんでした。 理由は、下記2点です

  1. 同じ画面でpageかlimitのstateを更新した後、データ操作後にmutate関数を呼んでも、最後に更新したpage, limitのキーに対してしか再検証が行われなかった
  2. 別の画面で上記のButtonコンポーネントを使用した時、データ作成などの処理を行った後に再検証の処理が走ってしまう

ちょっとよくわからないと思うので、下記にて具体的に説明します。

  1. 同じ画面でpageかlimitのstateを更新した後、データ操作後にmutate関数を呼んでも、最後に更新したpage, limitのキーに対してしか再検証が行われなかった

例えばpageを1から2にすると、SWRの第一引数に渡しているkeyはそれぞれ下記のようになります。

data?page=1&limit=10data?page=2&limit=10

この状態でデータを作成、編集、削除後にmutate関数を呼んでも、data?page=2&limit=10 のキーに対してしか再検証が行われませんでした。つまり、data?page=1&limit=10というキーに紐付けられたデータは、データ操作以前のままということになります。 これでは整合性が保てません。

2.別の画面で上記のButtonコンポーネントを使用した時、データ作成などの処理を行った後に再検証の処理が走ってしまう

別の画面でデータ作成などの処理を行った後すぐにデータを再取得してしまいますが、その画面ではそのデータは必要ないので再取得してほしくないです。データ作成などの処理後、そのデータが表示される画面にアクセスした際に、最新のデータを再取得してほしいですよね。

う〜ん悩ましい...。

解決策: 関連するキャッシュのみ削除する

いろいろ考えた結果、先述の2点の問題を一気に解決する方法として、「関連するキャッシュのみ全部削除」すればよいのでは?という結論に至りました。 今回で言えば、 data?page=${page}&limit=${limit} の条件を満たすキャッシュだけ削除してしまうということです。 そうすることで、

  1. 同じ画面でpage, limitなどのstateを変更後にデータを操作しても、関連するすべてのkeyに対して、必要なタイミングで再検証してくれそう
  2. 別の画面でデータを操作しても、その画面では再検証は行われず、リスト画面にアクセスした時に最新のデータをリクエストしてくれそう

です!

これを実装するいいやり方が無いか公式ドキュメントを探してみたところ、SWRver2.0から追加された下記を参考にすると良さそうでした!

参考: https://swr.vercel.app/ja/docs/advanced/cache#キャッシュを更新する

上記を参考にキャッシュの削除を実装すると、下記のようになります

export const Button ({ mutateKey }: ButtonProps) => {
    const { mutate } = useSWRConfig()

    const onClick = () => {
        //省略 データ作成、編集、削除処理

        mutate(key => typeof key === 'string' && key.startsWith('data?'),
            undefined,
            { revalidate: false }
        )
    }

    return (
        <button
            onClick={onClick}
        >
            ボタン
        </button>
    )
}

オプションで { revalidate: false} を指定すると、キャッシュを削除するだけで再検証は行われないそうです!

バージョン1.xxであれば、下記のようなやり方が使えます

https://swr.vercel.app/ja/docs/advanced/cache#正規表現から複数のキーを変更する

これにならってカスタムフックなどを作って使うと良さそうです!

export const useSWRCache = () => {
    const { cache, mutate } = useSWRConfig();

    const matchedFilter = useCallback(
        (matcher: RegExp) => {
            if (!(cache instanceof Map)) {
                throw new Error('matchMutate requires the cache provider to be a Map instance')
            }
            const keys = Array.from<string>(cache.keys());
            return keys.filter((key) => matcher.test(key));
        },
        [cache]
    );

    const deleteMatchedCaches = useCallback(
        (matcher: RegExp) => {
            const matchedKeys = matchedFilter(matcher);
            Promise.all(
                matchedKeys.map((key) =>
                    mutate(key, undefined, { revalidate: false })
                )
            );
        },
        [matchedFilter, mutate]
    );

    return {
        matchedFilter,
        deleteMatchedCaches
    };
};

コンポーネント側

export const Button () => {
    const { deleteMatchedCaches } = useSWRCache()

    const onClick = () => {
        //省略 データ作成などの処理

        deleteMatchedCaches(/data\?/)
    }

    return (
        <button
            onClick={onClick}
        >
            button
        </button>
    )
}

このようにすることで、下記のような動作をするようになりました

  1. リスト画面にアクセス後、別画面でリスト画面に関するデータを操作した後にリスト画面に戻ってくると、そのタイミングで最新のデータを取得してくれる
  2. ページや表示件数を変更後にデータを操作すると、変更前のページや表示件数の画面にアクセスした時に最新のデータを取得してくれる

めでたしめでたし! ※ 都合上、実際の画面を見せることができないのは残念です…

まとめ

  • mutate関数に { revalidate: false } オプションを与えると再検証は行われないので、mutate関数を呼ぶ画面で再検証が不要なら使うと良さそう
  • keyをstateなどで可変にしている場合や複数の引数を使っている場合、関連するkeyのキャッシュをまとめて削除すると良さそう
    • SWR2.0からデフォルトでそれをサポートしてくれている
    • SWR1.xxの場合は自力で頑張るしかなさそう

最後に

適切なタイミングで必要な分のみキャッシュを削除するというのは、他にも色々応用が効きそうだと思いました。キャッシュについては、まだあまり詳しくわかっていないので今後も学習を続けていきたいなと思いました。

AlphaDrive では、一緒にプロダクトを開発する仲間を募集中です! カジュアル面談もやっています。「ちょっと話をきいてみようかな〜」と思っていただけたら、気軽にご連絡ください!! エンジニア大募集中ですので、ぜひよろしくお願いします!

Page top