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 useCountState
1ファイル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つのライブラリで管理したいという方は検討してみてはいかがでしょうか?