thumbnail

SWRでGlobal Stateを実装する

2022/08/30

フロントエンドのstate管理はここ数年で様々な変遷を得て来ました。 近年、swrapollo clientのようなサーバーデータをキャッシュするライブラリが出てからは、state管理がかなり最適化されたと感じています。 swrなどが出て以降のフロントのstateはこちらの記事で言及されているように

  • サーバーデータのキャッシュ
  • Global State
  • Local State

の3つで構成されるパターンが増えてきました。 上の記事では

「SPAで管理する必要のあるGlobal Stateって、そのうちほとんどがサーバーデータのキャッシュだよね。それを取り除けたら、管理する必要のあるGlobal Stateってすごく小さくなるんじゃない?」

と言及されていますが、個人的にも同意見で、この構成がstate管理の最適解に近いと感じています。 実際、サーバーデータをswrなどがいい感じにキャッシュしてくれるため、Global Stateの管理が随分楽になりました。

サーバーデータのキャッシュ管理にはswrapollo clientが、Local State管理にはReact.useStateが主に使われますが、Global State管理に使用できるライブラリには様々なものがあります。 最近だとRecoiljotai, 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にする具体的なコードが以下になります。

useStaticSWR.ts
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管理するときの一例です。

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

author picture

Mitsuru Takahashi

京都市内にてフリーランスエンジニアとして活動しています。

detail

Profile

author picture

Mitsuru Takahashi

京都市内にてフリーランスエンジニアとして活動しています。

detail

© 2022 mitsuru takahashi