
SWRでGlobal Stateを実装する
2022/08/30
フロントエンドのstate管理はここ数年で様々な変遷を得て来ました。
近年、swr、apollo clientのようなサーバーデータをキャッシュするライブラリが出てからは、state管理がかなり最適化されたと感じています。
swrなどが出て以降のフロントのstateはこちらの記事で言及されているように
- サーバーデータのキャッシュ
- Global State
- Local State
の3つで構成されるパターンが増えてきました。 上の記事では
「SPAで管理する必要のあるGlobal Stateって、そのうちほとんどがサーバーデータのキャッシュだよね。それを取り除けたら、管理する必要のあるGlobal Stateってすごく小さくなるんじゃない?」
と言及されていますが、個人的にも同意見で、この構成がstate管理の最適解に近いと感じています。
実際、サーバーデータをswrなどがいい感じにキャッシュしてくれるため、Global Stateの管理が随分楽になりました。
サーバーデータのキャッシュ管理にはswr、apollo clientが、Local State管理にはReact.useStateが主に使われますが、Global State管理に使用できるライブラリには様々なものがあります。
最近だとRecoil、jotai, zustandなどが流行っており、先ほど紹介した記事でもGlobal State管理にはRecoilを使っているようです。
流行りのライブラリをつかってGlobal Stateを実現してもよいのですが、サーバーデータキャッシュライブラリであるswrでも実はGlobal State管理が可能だったりします。
projectに導入するstate管理ライブラリをあまり増やしたくない方向けに、swrでの個人的なGlobal State管理方法を紹介したいと思います。
方法
Global State用にuseSWRをカスタマイズ
useSWR自体はキャッシュ管理(global state管理) + データ再検証(apiリクエスト)機能をあわせたようなライブラリなので、useSWRからこの再検証機能をoffにしてあげることで、global stateとしての運用が可能になります。
useSWRの再検証機能をoffにする具体的なコードが以下になります。
import { useEffect } from 'react'
import useSWR from 'swr'
const useStaticSWR = <T>(key: string, initialData: T) => {
const { data = initialData, mutate } = useSWR<T>(key, null, {
fallbackData: initialData,
revalidateOnFocus: false, // 画面フォーカス時の再検証(apiリクエスト)をオフ
revalidateOnMount: false, // コンポーネントマウント時の再検証(apiリクエスト)をオフ
revalidateOnReconnect: false, // ブラウザがネットワーク接続できた時の再検証(apiリクエスト)をオフ
revalidateIfStale: false, // キャッシュが古くなったときの再検証(apiリクエスト)をオフ
})
useEffect(() => {
// swrでキャッシュされているdataがundefinedだった場合に、initialDataをset
mutate((_data) => _data || initialData, false)
}, [])
return { data, mutate }
}
export default useStaticSWR再検証機能はuseSWRのオプション上にあるrevalidate*系をfalseにすることでオフにすることができます。
また、useSWRでのdata初期値は必ずundefinedなので、そのときのfallbackDataとしてinitialDataを指定しています。
これにより、初期値がundefinedだったときにfallbackDataが返ってくるので、useSWRでも擬似的に初期値を設定することが可能になります。
(ただし、swrのキャッシュ内部的には初期dataはundefinedとなっているので、useEffectで明示的にinitalStateをsetしています。)
ファイルごとにstateを管理
上記で定義したuseStaticSWRを使用して、global state定義を行います。
以下はcountをglobal state管理するときの一例です。
import { useCallback } from 'react'
import useStaticSWR from './useStaticsSWR'
type CountState = {
count: number
}
const initialState: CountState = {
count: 0,
}
const useCountState = () => {
const { data: countState, mutate } = useStaticSWR<CountState>(
'countState',
initialState,
)
const setCountState = useCallback(
(data: Parameters<typeof mutate>[0]) => {
mutate(data, false)
},
[mutate],
)
return {
countState,
setCountState,
}
}
export default useCountState1ファイル1global stateとして定義しており、global stateにアクセスしたいcomponent上で、useCountState()するという形で運用しています。
シンプルで使いやすいのですが、SWRでGlobal Stateを管理する際の注意点として以下2つがあります。
useStaticSWRはdataがundefined時にfallbackDataを返すため、undefinedは定義できないmutate((prev) => { ...prev, newData })のprevの型にundefinedが入ってしまう
上記の点以外での使用感は他のglobal state管理ライブラリと変わらないです。 キャッシュを含めたglobal state系はすべて1つのライブラリで管理したいという方は検討してみてはいかがでしょうか?
