SWRでログアウト時やユーザー切り替え時に「キャッシュを全部消したい」場面、ありますよね。今回は、そこで盛大にハマった話です。
最初にやったこと
まず思いついたのがこれ。
const { cache } = useSWRConfig();
// これはNG
for (const key of cache.keys()) {
cache.delete(key);
}
cacheがMapっぽいしkeys()もdelete()もあるし、「これで全消しできるでしょ」と思いました。
ここから沼が始まります。
実際に起きた謎現象
① isLoadingが永遠にtrueなのに通信が飛ばない
これが一番怖いやつ。
const { data, isLoading } = useSWR('/api/hoge', fetcher);
isLoading === truedata === undefined- Networkタブ:何も飛んでない
- エラーも出ない
つまり、「今ロード中です(嘘)」 という状態。
UIは永遠にスピナーが回り続けます。fetcherもAPIも問題ないのに、SWRが沈黙するという最悪の状態でした。
② mutate()で明示的に呼ばないとデータが取得できない
mutate('/api/user');
このように手動でmutateしたものだけは取得できる。
しかし、普通のuseSWRは一切データを取りに行かない。
// これが動かない
useSWR('/api/hoge', fetcher);
なぜこうなったのか
cache.delete(key)はMapライクな操作ですが、SWRの内部状態($req、$len、$infなど)は別管理されています。
// cache.delete()で消えるのはキャッシュの「値」だけ
// SWR内部の状態管理は壊れたまま残る
// → 状態の不整合が発生
結果としてSWRは「このkeyはもう処理済み」と誤認し、fetchを投げないままisLoadingがtrueで固まる半壊状態に陥りました。
正解:mutate()で全てundefinedにする
正解はこれでした。
const { mutate } = useSWRConfig();
// 全てのキャッシュを正しく削除する
mutate(
(key) => true, // すべてのキーにマッチ
undefined, // キャッシュの値をクリア
{ revalidate: false } // 消した直後に再fetchしない
);
これにより、キャッシュの更新・状態遷移・useSWRへの通知がSWRの正規ルートで処理されます。
ちなみにrevalidate: trueにすると、キャッシュクリア直後に全キーで再fetchが走ります。ログアウト時など「消すだけでいい」場面ではfalseが適切です。
オチ:普通にReferenceに書いてあった
散々ハマってから公式ドキュメントを読み直したところ、
"In most cases, you shouldn't directly write to the cache, which might cause undefined behaviors of SWR."
しれっと書いてありました。
まとめ
-
cache.delete()で全消しすると状態が壊れる -
isLoadingがtrueのまま通信が飛ばない半壊状態になる - 正解は
mutate((key) => true, undefined, { revalidate: false }) - 答えは公式Referenceに書いてあった
ドキュメントは先に読みましょう。