⚡️

useSWRをuseContextの代わりに使ってみる試み

2024/06/14に公開

先日、こちらの勉強会に参加しました。

https://classmethod.connpass.com/event/316669/

学びになる内容がたくさんあったのですが、自分が一番気になったのはこちらの発表でした。

https://speakerdeck.com/sutetotanuki/gao-su-an-jian-li-tishang-gedeshi-warerumatuhatenpuretonohurontoendoji-shu-xuan-ding?slide=36

この発表のなかで触れられている、

必要に応じてSWRのfetch関数にnullを指定、キャッシュのインバリデートを無効にしたグローバルのキャッシュ領域をグローバルステートとして利用する方針にしている

という部分に「なるほど」となりつつも「どう書く🤔」となったため、それを試してみたというのが本記事の内容です。

SWRとは

https://swr.vercel.app/ja

Next.jsを作成しているVercel社が作っているデータ取得のための React Hooks ライブラリです。

基本的な使い方はこんな感じです。

swrの基本的な使い方
import useSWR from 'swr'
 
function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)
 
  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

useSWRフックは通常APIのURLとなる一意な識別子と、 データを返す任意の関数でネイティブの fetch や Axios を内包したコールバックを渡します。

その他いろいろとできることはありますが、今回の記事ではこの基本だけ理解しておけば大丈夫です。

他の機能については公式ドキュメントに譲ります。

useSWRをuseContext風に使ってみる

先のスライドの例は再検証を無効にしているので、初期値を取得してから値は基本的に変わりません。

なので用途としては、ログイン時に取得した認可情報を持ち回ったりというのに使うものと思われます。

まずは最初に取得してから常に同じ値をキャッシュから取得するパターンを見てみたいと思います。

更新を伴わないcontextとして使う

import './App.css'
import useSWR from "swr"
import { useState } from "react"

const KEY = "weather-info"

const fetcher = async () => {
  const res = await fetch("https://weather.tsukumijima.net/api/forecast/city/280010")
  const data = await res.json()
  return data.description.bodyText
}

const Children = () => {
  const { data } = useSWR(KEY, null); //fetcherをnullとし、常に親コンポーネントの値を参照する
  return (
    <div className="card">
      <p className="read-the-docs">
        {data}
      </p>
    </div>
  );
}

function App() {
  const [count, setCount] = useState(0);
  const { data } = useSWR(KEY, fetcher); //親側でキャッシュする情報を取得する
  return (
    <>
      <h1>Parent</h1>
      <div className="card">
        <button onClick={() => {
          setCount((count) => count + 1);
        }}>
          count is {count}
        </button>
        <p className="read-the-docs">
          {data}
        </p>
      </div>
      <div className="card">
        <h2>Children</h2>
        <Children />
      </div>
    </>
  )
}

export default App

ここでは同じキーを持ち回ります。親コンポーネントにおいて、初回レンダリング時のみ天気予報の情報を取得することになります。

2回目以降は親コンポーネントのレンダリングが発生しても、キャッシュが効いて再度fetchが実行されることはありません。

子コンポーネントは、初回レンダリング時に取得した天気予報の情報が常に取得されることになります。

このことをカウンターボタンを押して再レンダリングを発生させながら確認したいと思います。

まずは初回レンダリングのタイミングです。ネットワークタブを確認すると、一回fetchが走ったことが確認できます。

この状態からcount is 0のボタンを押すと…?

カウントはアップしましたが、fetchは走っていません。また子コンポーネントには同じ値が表示されています。

更新を伴うcontextとして使う

contextの用途として、親コンポーネントでの画面での変更を複数の子孫で共有したい場合もあり得ます。

その場合についても使い方を見てみます。

import { useEffect, useState } from 'react'
import './App.css'
import useSWR, { useSWRConfig } from 'swr'

const COUNTER_KEY = 'counter-key'

const Children = () => {
  const { data } = useSWR(COUNTER_KEY, null)
  return (
    <div className="card">
      <h3>Children</h3>
      count is {data} in children
    </div>
  )
};

function App() {
  const [count, setCount] = useState(0)
  const { mutate } = useSWRConfig()
  const { data } = useSWR(COUNTER_KEY, () => count, {
    revalidateOnFocus: false,  // フォーカス時の再検証を無効にする
    revalidateOnReconnect: false,  // 再接続時の再検証を無効にする
    refreshInterval: 0,  // 自動リフレッシュを無効にする
  })

  useEffect(() => {
    mutate(COUNTER_KEY, count)
  }, [count, mutate])

  return (
    <>
      <h1>Parent</h1>
      <div className="card">
        <button onClick={() => {
          setCount((count) => count + 1);
        }}>
          swr count is {data}
        </button>
        <p>
          state count is {count}
        </p>
      </div>
      <Children />
    </>
  )
}

export default App

ここではCOUNTER_KEYというキーでもって、親コンポーネントのカウンターの値を持ち回ります。

親コンポーネントはというと、useEffectにてcountの値が変わったタイミングでキャッシュの値も更新するようにしています。

では動きをみます。まずは初期表示ですが、カウンターの数値は親子共に0です。

ここでカウンターを押すと…

子コンポーネントの値も1に変わりました。

おわりに

だいたいこんなイメージで使えば、多くの場面でuseContextを使うことなくグローバルに状態を共有できそうです。

ライブラリの導入を極力抑えることができれば、セキュリティ面でも安心感が増すので、今回のようにuseContextの代わりにuseEffectを使うことを検討してみてもよいのかもしれません。

ちなみにですが、結局useSWRのキャッシュ管理ではuseContextを使っているようなので、直接使うか、間接的に使うかだけの違いのようです。

https://zenn.dev/edash_tech_blog/articles/147a8eefc377c2

📢 Kobe.tsというTypeScriptコミュニティを主催しています

フロント・バックエンドに限らず、周辺知識も含めてTypeScriptの勉強会を主催しています。

毎朝オフラインでもくもくしたり、神戸を中心に関西でLTもしています。

盛り上がってる感を出していきたいので、良ければメンバーにだけでもなってください😣

https://kobets.connpass.com/

Discussion