🔖

SWRConfig を使うと global mutate が使えない問題を解決する

2023/11/04に公開

TL;DR

initCache を利用して cache と mutate のペアを新たに生成して使うと良い。

はじめに

https://swr.vercel.app

swr は内部にインメモリキャッシュを持っており、同じ key を指定するとデータを共有する。stale-while-revalidate に由来した自動更新機能も内包しており、手軽に使えるデータフェッチライブラリである。

swr は SWRConfig を利用してインメモリキャッシュを設定することもできる。しかし、独自に設定するとグローバルミューテートが使用できなくなるのでそれを今回は解決していこうと思う。

https://github.com/vercel/swr/issues/2799

global mutate を実行してもキャッシュが更新されてない様子

const config = {
  provider: () => new Map(),
};

const App = () => {
  return (
    <SWRConfig value={config}>
      <Suspense>
        <Component />
      </Suspense>
    </SWRConfig>
  );
};

const getData = () => Promise.resolve({ ts: Date.now() });
const Component = () => {
  const { data } = useSWR("data", getData, { suspense: true });
  const handleClick = useCallback(async () => {
    await mutate("data", await getData());
  }, []);

  return (
    <div>
      <p>timestamp: {data.ts}</p>
      <button onClick={handleClick}>mutate</button>
    </div>
  );
};

swr のデフォルトのキャッシュはどこから来ている?

swr のキャッシュを知るためにどこでキャッシュを作っているかを探してみる。

これが import useSWR from 'swr' の中身。

https://github.com/vercel/swr/blob/main/core/src/use-swr.ts#L764

この中で cache が利用されているところを見てみる。

この辺りがキャッシュからデータを読んでるところ

https://github.com/vercel/swr/blob/main/core/src/use-swr.ts#L241-L255

どうやら useSWRHandler の第三引数の config からキャッシュを取得しているようだ。

https://github.com/vercel/swr/blob/main/core/src/use-swr.ts#L97

普段 useSWR を利用する上で config を明示的に指定することはないがここでは必須になっている。withArgs から注入されているみたいなのでそちらを見てみる。

https://github.com/vercel/swr/blob/main/_internal/src/utils/resolve-args.ts#L11

あった。useSWRConfig から config は注入されているようだ。

https://github.com/vercel/swr/blob/main/_internal/src/utils/use-swr-config.ts#L8

useSWRConfig 内部では useContext が利用されているので、最も近い SWRConfig を探して config を返す。

swr のキャッシュは SWRConfig で provider が設定されていればそちらを、なければ defaultConfig の provider を参照することが分かった。

https://github.com/vercel/swr/blob/main/_internal/src/utils/config.ts#L44

ここが defaultConfig 内のキャッシュを作成している場所。initCache 関数を利用して cache と mutate のペアを作成している。

キャッシュを差し替えたうえでグローバルミューテートを使う

先ほどキャッシュの在り処を調べたことで、swr が提供している mutate 関数は defaultConfig に刺さっている cache のみを向いていることが分かった。

ならば、SWRConfig に設定するキャッシュを initCache で作成し、cachemutate のペアを新たに作りそちらを利用すれば良い。

import { initCache } from "swr/_internal";
import type { ScopedMutator, Cache } from "swr/_internal";

const [cache, mutate] = initCache(new Map()) as [Cache<unknown>, ScopedMutator];

const config = {
  provider: () => cache,
};

const App = () => {
  return (
    <SWRConfig value={config}>
      <Suspense>
        <Component />
      </Suspense>
    </SWRConfig>
  );
};

const getData = () => Promise.resolve({ ts: Date.now() });
const Component = () => {
  const { data } = useSWR("data", getData, { suspense: true });
  const handleClick = useCallback(async () => {
    await mutate("data", await getData());
  }, []);

  return (
    <div>
      <p>timestamp: {data.ts}</p>
      <button onClick={handleClick}>mutate</button>
    </div>
  );
};

うごいた。

Scoped Mutate が動いているさま

一応 docs を更新する PR を出している。

https://github.com/vercel/swr-site/pull/550

Discussion