SWRをローカルの状態管理で使うときの注意
下記の記事でSWRでグローバルstateを管理する方法が気になった。
上記を元にカスタマイズしつつ、検証しつつ遊んでみる。
まずはuseSWR
を使った特定のkey専用のロジックを定義。
export const useSWRSample = ( initailData: string ): { data: string | undefined; mutate: (updateData: string) => void } => { const { data, mutate } = useSWR("testKey", null, { initialData: initailData, }); return { data: data, mutate: mutate }; };
使いたいコンポーネントで呼び出し、初期化。
const { data, mutate } = useSWRSample("initaial data"); console.log(data); // initaial data
mutateで値を更新
<button onClick={() => { mutate("updated data"); }} >
console.log(data); // updated data
上記の値を別のコンポーネントでの読み込みは参考記事では、単純なuseSWR
を使っていたので真似する。
SWRの性質を利用してkeyさえあっていれば同様の値を読める。
const { data } = useSWR("testKey", null); console.log(data); // updated data
ただ、上記では問題があり、更新後のupdated data
は読み込めるが、初期値のinitial data
は読み込めずundefined
になる。
多分これは上記2つのコンポーネントが同ページで読み込まれたときに、初期化処理の前に別コンポーネントで読み込みをするため。
なので、その後mutateすると、同期されて2つのコンポーネントでupdated data
となる。
その証拠に、しれっと参考記事では下記のようにundefined
の際の対策がされている。
const SampleCount = () => { const { data: count } = useSWR('count', null); return <div>{count || 0}</div>; };
{count || 0}
の部分で、count
がfalsyだった場合は初期値の0をセットしている。あまり書き方として筋はよくなさそう。
ちなみに、このままSPA遷移でページを移動しても、値は保持され続ける。 いわゆるstoreのようにグローバルstateとして扱えるイメージ。 storageと組み合わせた半永続化の術もあるかもだが、上記の状態ではページ読み込みすると値が破棄される。
似たような別記事があったので見てみる
この記事ではuseStaticSWR
のように、SRWの第2引数をnullに設定したカスタムのhooksを作っている。
これを使えばSWRでローカルでの状態管理を使える汎用的なものになっている。
import useSWR, { Key, SWRResponse, mutate } from 'swr'; import { Fetcher } from 'swr/dist/types'; export const useStaticSWR = <Data, Error>(key: Key, updateData?: Data | Fetcher<Data>): SWRResponse<Data, Error> => { if (!updateData) { mutate(key, updateData); } return useSWR(key, null, { revalidateOnFocus: false, revalidateOnReconnect: false, }); };
そして各状態毎にuseStaticSWR
を利用した、専用のhooksを作っている構成。例えば下記のように。
export const useOgpCardLayout = (initialData?: OgpLayoutType): SWRResponse<OgpLayoutType | null, Error> => { return useStaticSWR<OgpLayoutType | null, Error>('ogpCardLayout', initialData); };
https://github.com/webev-dev/webev-front/blob/master/src/stores/contexts.tsx
前記事でも一番気になった、初期化後に各コンポーネントで状態を同期する方法を調査。
useStaticSWR
を利用した、専用のhooksは3種類あるので、それぞれ調査。
まずuseOgpCardLayout
は、下記のようにコンポーネント読み込み初期時にmutateを実行している。
この処理によって同期している気がする。
useEffect(() => { setIsEnableReadFromClipboard(retrieveValue('isEnableReadFromClipboard') === 'true'); mutateOgpCardLayout(retrieveValue<OgpLayoutType>('cardLayout')); }, []);
2つ目はuseUrlFromClipBoard
、
下記のreadClipboardText
中でmutateを実行。
windowが依存配列になっているが、useEffectが初期時も実行されるはずなので、結局初期時に同期してる気がする。
useEffect(() => { if (typeof window !== 'undefined') { window.addEventListener('focus', readClipboardText); } return () => { if (typeof window !== 'undefined') { window.removeEventListener('focus', readClipboardText); } }; }, [window]);
3つ目は、useSocketId
で、こちらも初期時にmutateしている。
useEffect(() => { socket.on('connect', () => { console.log('socket connected!!'); }); socket.on('disconnect', () => { console.log('socket disconnected!!'); }); socket.on('update-page', () => { console.log('Get Updated Data'); pageListMutate(); }); socket.on('issue-token', ({ socketId }: { socketId: string }) => { mutateSocketId(socketId); }); return () => { socket.close(); }; }, []);
結果、どれもコンポーネント初期読み込み時にmutateしていた。 多分これをしないと、全呼び出し元で初期データが同期されないはず。
まとめ