【React】useSWRはAPIからデータ取得をする快適なReact Hooksだと伝えたい

更新日: 2020/10/29

@Panda_Program

Vercel製のuseSWRはReactの非同期データ取得をラクにする

SWRとは、Next.jsを作成しているVercel製のライブラリです。**SWRはuseSWRというReact Hooksを提供し、APIを通じたデータの取得をラクに記述する手助けをしてくれます。**このライブラリはなんとGitHubスター数を10,700も獲得しています。

SWRはライブラリ名で、stale-while-revalidateというRFC 5861で策定されたキャッシュ戦略の略称です。このSWRがデータ取得の扱いをラクにしてくれて最高なのです。

React開発者が嬉しいuseSWRの書き心地

useSWRは外部APIからのデータ取得、ローディング状態、エラーが発生した時をシンプルに記述できます。これがあらゆるReact開発者にとって(というか、ReactでAPIにリクエストを頻繁に送るアプリケーションを実務で書いている自分が)とても嬉しいことです。

例えば、Suspenseを使わず、useEffectで取得したデータをuseStateに格納して利用するコードがあるとします。

Profile.jsx
const Profile = () => {
  const [profile, setProfile] = useState(null)
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    (async () => {
      setLoading(true)

      const res = await fetch('/api/user')
      setProfile(await res.json())

      setLoading(false)
    })()
  }, [])

  if (profile === null || loading) {
    return <div>loading</div>
  }

  return <div>hello {profile.name}!</div>
}

記述量が多くて辛いですね。しかも通信エラーが起きた時のハンドリングをしていません。それでももうこの長さです。

しかし、useSWRを使うともっと短く記述できるんです。

import useSWR from 'swr'

const fetcher = () => fetch('/api/user')

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>

  return <div>hello {data.name}!</div>
}

**たったこれだけでローディング、通信エラー、データ取得の状態を表せます。**とってもシンプルなインターフェースです。

なお、useSWRの第一引数はキャッシュのキーです。第二引数は、データを取得する関数を返すPromiseです。

**嬉しいことに、useSWRは第二引数で与えたfetcherが一度取得したデータをクライアント側でキャッシュしてくれます。**これで、APIを通じて取得したデータをstoreに格納せずに済むのです。

useSWRによるキャッシュの流れ

useSWRがキャッシュからデータを取得する流れは以下です。

  • キャッシュからデータを返そうとする(Stale
  • キャッシュにデータがなければ、データを取得する
  • キャッシュにデータがあれば、再度データを取得してキャッシュを更新する(Revalidate

SWRは「stale-while-revalidate」の略でしたね。これだけを理解すればあなたはもうuseSWRを使いこなせること間違いなしです。

さらに嬉しいuseSWRの機能

useSWRはデフォルトでRevalidateに関する嬉しい機能を備えています。Revalidateは(データ取得+キャッシュ更新)を意味します。

  • ブラウザをクリックしたり、タブを移動して戻ってきたときにRevalidateする(Focus Revalidation)
  • 指定した時間ごとにRevalidateする(pollingができる)
  • mutate関数を使ってデータ更新時にキャッシュも更新できる(Local mutation)
  • 無限スクロールの場合、ページ遷移後もスクロール位置を保存する
  • エラー時にリトライしてくれる
  • タイムアウトの設定が簡単

よく使うオプションを紹介します

useSWRを使う際によく使うであろうオプションを紹介します。

|プロパティ|役割|デフォルト値| |--|--|--| |initialData|データfetch前に表示する初期データ|なし| |revalidateOnFocus|windowのフォーカス時にRevalidateする|true| |revalidateOnReconnect|通信が切れて復活したらRevalidateする|true| |refreshInterval|pollingの期間|0(pollingしない)| |focusThrottleInterval|1度だけRevalidateする期間|5000| |loadingTimeout|タイムアウトする時間|3000|

エラー時の対応も柔軟に設定できます。

|プロパティ|役割|デフォルト値| |--|--|--| |shouldRetryOnError|エラー時にリトライするか|true| |errorRetryInterval|リトライのインターバル|5000| |errorRetryCount|リトライするmaxの回数|なし|

useSWRの第三引数にオプションとして指定することで、Revalidateを柔軟に設定できます。

const { data, error } = useSWR('/api/user', fetcher, {
  initialData: { name: 'React Developer' }
})

全てのoptionを確認するにはSWRのREADMEをご覧ください。

useSWRはSSRで利用できる

useSWRはSSR(Sever Side Rendering)で利用できます。ただし、optionのinitialDataの指定をしないとバグの原因になります。

Next.jsのgetServerSidePropsというサーバーでしか実行されない関数と一緒に用いた例は以下の通りです。

export async function getServerSideProps() {
  const data = await fetcher('/api/data')
  return { props: { data } }
}

function App (props) {
  const initialData = props.data
  const { data } = useSWR('/api/data', fetcher, { initialData })

  return <div>{data}</div>
}

getServerSidePropsでNext.jsのビルド時にAPIからデータを取得し、それを初期データとして表示します。

しかし、ユーザーのプロフィールなどをユーザー自身が編集したときデータは更新されます。

クライアントでuseSWRを使うことで、バックエンドでデータ更新があった時もSWRがRevalidateしてくれるので、常に最新のデータをユーザーに表示できるのです。

useSWRはGraphQLに対応している

また、SWRはGraphQLにも対応しています。サンプルコードを掲載しておきます。

import { request } from 'graphql-request'
import useSWR from 'swr'

const API = 'https://api.graph.cool/simple/v1/movies'

function MovieActors() {
  const { data, error } = useSWR(
    `{
      Movie(title: "Inception") {
        actors {
          id
          name
        }
      }
    }`,
    (query) => request(API, query)
  )

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return data.Movie.actors
    .map((actor) => <li key={actor.id}>{actor.name}</li>)
}

SWRのコードを読んでキャッシュの仕組みを理解する

SWRのキャッシュの仕組みを知るために、実際にSWRのコードを読んでみましょう。index.tsのuseSWRの定義でcache.set(key, newData, false)とあり(322行目)、cacheにデータを格納していますね。

index.ts
function useSWR<Data = any, Error = any>(
  key: keyInterface,
  fn?: fetcherFn<Data>,
  config?: ConfigInterface<Data, Error>
): responseInterface<Data, Error> {
  // ...
  // start a revalidation
  const revalidate = useCallback(
    async (
      revalidateOpts: RevalidateOptionInterface = {}
    ): Promise<boolean> => {
      // ...
      cache.set(key, newData, false)
      cache.set(keyErr, undefined, false)
      // ...
      return true
    },
    [key]
  )
  // ...
  return useMemo(() => {
    // ...
  }, [revalidate])
}

このcacheを追ってみます。cacheはconfig.tsからimportされていますので、config.tsを見てます。

config.ts
import Cache from './cache'
// cache
const cache = new Cache()

export {
  // ...
  cache
}

そして、cache.tsには以下のようにCacheクラスの定義がありました。

cache.ts
export default class Cache implements CacheInterface {
  private __cache: Map<string, any>

  constructor(initialData: any = {}) {
    // Mapオブジェクトを生成している
    this.__cache = new Map(Object.entries(initialData))
  }

  get(key: keyInterface): any {
    const [_key] = this.serializeKey(key)
    return this.__cache.get(_key)
  }

  set(key: keyInterface, value: any, shouldNotify = true): any {
    const [_key] = this.serializeKey(key)
    this.__cache.set(_key, value)
    if (shouldNotify) mutate(key, value, false)
    this.notify()
  }

  keys() {
    return Array.from(this.__cache.keys())
  }

  serializeKey(key: keyInterface): [string, any, string] {
    // keyが関数や配列の時にシリアライズする処理
    return [key, args, errorKey]
  }
}

コンストラクタでthis.__cache = new Map(Object.entries(initialData))と定義されていますね。これがSWRのキャッシュの正体です。

Mapとは、key/Valueのデータ構造で、HashやHashMapとも呼ばれます。値はキーに対して、常に一意に決まります。

「useSWRがクライアントでキャッシュする」という意味は、ブラウザで実行するJavaScriptがメモリ上に揮発性のデータを確保するということです。

まとめ

いかがでしたでしょうか。useSWRはReactでデータフェッチを楽に扱える強力なHooksです。

実務ではFetchしてきたデータをstoreに格納していますが、SWRを使ってクライアントキャッシュに格納するように書き換えています。

SWRはVercel製ということもあり、そのシンプルなインターフェースはVercelのプロダクト「Vercel」や「Next.js」に共通しているZero Configを彷彿とさせるものです。

ぜひ本番環境でも使ってみてくださいね。

Happy Coding 🎉

この記事が「役に立つ」「面白い」と思ったら、下記のボタンから Twitter でのシェアやはてなブックマークをお願いします!

  • Share on Hatena
  • Share on Twitter
  • Share on Line
  • Copy to clipboard