状態管理ライブラリの分類

クライアント状態 ← → サーバー状態(API)
クライアント × グローバル
Zustand Jotai Redux Context API
例: テーマ、モーダル状態、カート
サーバー × グローバル
React Query SWR Apollo Client
例: ユーザー情報、商品一覧、投稿データ
クライアント × ローカル
useState useReducer
例: フォーム入力、開閉状態、カウンター
サーバー × ローカル
fetch + useState useEffect内でAPI
例: 単発のAPI呼び出し(キャッシュ不要)
↑ グローバル ローカル ↓
選び方の指針
  • • APIから取得するデータ → React Query / SWR
  • • 複数コンポーネントで共有するUI状態 → Zustand / Jotai
  • • 単一コンポーネント内の状態 → useState
目次

ReactのデータフェッチングをSWRでシンプルにする

SWRは、Vercelが開発したReact向けのデータフェッチングライブラリだ。名前はHTTPキャッシュ戦略の「stale-while-revalidate」に由来している。

この戦略の動作は単純で、まずキャッシュにあるデータを即座に返し(stale)、裏側でAPIリクエストを飛ばし(revalidate)、最新データが取れたら画面を差し替える。ユーザーから見ると、画面遷移のたびにローディングスピナーが出ることなく、常にデータが表示されている状態になる。

useEffect + useStateで自前管理していたデータフェッチングのボイラープレートを、useSWRフック一つに置き換えられる。キャッシュ管理、再検証のタイミング制御、エラーハンドリング、重複リクエストの排除までライブラリ側が面倒を見てくれる。

SWRのデータ取得フロー

SWRがリクエストを処理する流れを整理する。

useSWR(key, fetcher) キャッシュを確認 keyに対応するデータがあるか キャッシュあり キャッシュなし staleデータを返す 即座にUIへ反映 isLoading: true 初回読み込み状態 fetcher実行(revalidate) バックグラウンドでAPI呼び出し キャッシュ更新 + UI反映 最新データで画面を差し替え

キャッシュがある場合は古いデータを即座に表示してからバックグラウンドで再取得する。キャッシュがない初回アクセスではisLoadingがtrueになり、通常のローディング状態を経てからデータが表示される。

基本的な使い方

インストール

npm install swr

useSWRフックの基本形

import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(res => res.json())

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

  if (error) return <div>エラーが発生しました</div>
  if (isLoading) return <div>読み込み中...</div>
  return <div>{data.name}のプロフィール</div>
}

useSWRの第1引数は「キー」で、キャッシュの識別子かつfetcherに渡される値になる。第2引数のfetcherはデータを取得する関数で、Promiseを返せば何でもよい。fetchに限らず、axiosでもGraphQLクライアントでも使える。

返り値のdataはフェッチ結果、errorはスローされたエラー、isLoadingはデータもキャッシュもない初回読み込み中かどうかを示す。

グローバル設定

fetcherを毎回渡すのは面倒なので、SWRConfigでアプリ全体のデフォルトを設定できる。

import { SWRConfig } from 'swr'

const fetcher = (url: string) => fetch(url).then(res => res.json())

function App() {
  return (
    <SWRConfig value={{ fetcher }}>
      <Dashboard />
    </SWRConfig>
  )
}

これ以降、各コンポーネントのuseSWRではキーだけ渡せばよい。

function Dashboard() {
  const { data } = useSWR('/api/dashboard')
  // ...
}

キャッシュと再検証の仕組み

SWRのキャッシュはインメモリで管理される。同じキーに対するuseSWRは、コンポーネントが別の場所にあっても同じキャッシュを共有する。これにより、ヘッダーのユーザー名とプロフィールページのユーザー情報が常に同期される。

再検証が走るタイミングはデフォルトで3つある。

トリガー説明オプション
フォーカス時ブラウザタブに戻ったときrevalidateOnFocus
ネットワーク復帰時オフラインからオンラインに戻ったときrevalidateOnReconnect
マウント時コンポーネントがマウントされたときrevalidateOnMount

不要な再検証を抑えたい場合は個別にオフにできる。

const { data } = useSWR('/api/settings', fetcher, {
  revalidateOnFocus: false,
  revalidateOnReconnect: false,
})

ポーリング

リアルタイム性が求められるデータには、refreshIntervalでポーリングを設定できる。

const { data } = useSWR('/api/notifications', fetcher, {
  refreshInterval: 3000, // 3秒ごとに再取得
})

ミューテーションと楽観的更新

データの読み取りだけでなく、更新後にキャッシュを反映させる仕組みも用意されている。mutate関数でキャッシュを書き換えられる。

基本的なミューテーション

import useSWR, { mutate } from 'swr'

async function updateUser(newName: string) {
  await fetch('/api/user', {
    method: 'PUT',
    body: JSON.stringify({ name: newName }),
  })
  // APIリクエスト後にキャッシュを再検証
  mutate('/api/user')
}

楽観的更新

APIのレスポンスを待たずにUIを先に更新し、失敗時にロールバックするパターンも書ける。

import useSWRMutation from 'swr/mutation'

function UserProfile() {
  const { data } = useSWR('/api/user', fetcher)
  const { trigger } = useSWRMutation('/api/user', updateUser)

  async function handleUpdate(newName: string) {
    await trigger(newName, {
      optimisticData: { ...data, name: newName },
      rollbackOnError: true,
    })
  }

  return <div>{data?.name}</div>
}

async function updateUser(url: string, { arg }: { arg: string }) {
  return fetch(url, {
    method: 'PUT',
    body: JSON.stringify({ name: arg }),
  }).then(res => res.json())
}

optimisticDataで即座にUIに反映し、rollbackOnError: trueでAPIが失敗した場合に元のデータに戻す。ユーザーはラグを感じずに操作でき、エラー時も整合性が保たれる。

条件付きフェッチ

キーにnullを渡すか、関数でnullを返すと、SWRはリクエストを送らない。依存するデータが揃ってから取得したいケースで使う。

function UserPosts({ userId }: { userId: string | null }) {
  // userIdがnullのときはフェッチしない
  const { data } = useSWR(
    userId ? `/api/users/${userId}/posts` : null,
    fetcher
  )
  return <div>{data?.length ?? 0}件の投稿</div>
}

依存フェッチにも応用できる。

function UserDashboard() {
  const { data: user } = useSWR('/api/user', fetcher)
  // userが取得できてから、そのIDでpostsを取得
  const { data: posts } = useSWR(
    user ? `/api/users/${user.id}/posts` : null,
    fetcher
  )
  // ...
}

無限ローディング(ページネーション)

useSWRInfiniteを使うと、ページネーションや無限スクロールを実装できる。

import useSWRInfinite from 'swr/infinite'

function PostList() {
  const getKey = (pageIndex: number, previousPageData: any[]) => {
    if (previousPageData && previousPageData.length === 0) return null
    return `/api/posts?page=${pageIndex + 1}&limit=10`
  }

  const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher)

  const posts = data ? data.flat() : []
  const isEnd = data && data[data.length - 1]?.length < 10

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
      {!isEnd && (
        <button onClick={() => setSize(size + 1)}>
          もっと読む
        </button>
      )}
    </div>
  )
}

getKey関数がページごとのキーを生成する。前のページのデータが空配列ならnullを返してフェッチを止める。setSizeでページ数を増やすと、次のページを取得する。

エラーハンドリングとリトライ

SWRはデフォルトでエラー時にリトライする。リトライ間隔は指数バックオフで増えていく。

const { data, error } = useSWR('/api/data', fetcher, {
  onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
    // 404はリトライしない
    if (error.status === 404) return
    // 最大10回まで
    if (retryCount >= 10) return
    // 5秒後にリトライ
    setTimeout(() => revalidate({ retryCount }), 5000)
  },
})

onErrorRetryでリトライのロジックを完全にカスタマイズできる。ステータスコードに応じてリトライを止めたり、間隔を調整したりする。

SWRとTanStack Queryの使い分け

SWRと同じ領域のライブラリとしてTanStack Query(旧React Query)がある。どちらを選ぶかは、プロジェクトの性質によって変わる。

観点SWRTanStack Query
バンドルサイズ約4KB(gzip)約13KB(gzip)
APIの設計思想シンプル、最小限網羅的、多機能
ミューテーションmutate + 手動管理useMutationで体系的に管理
Devtoolsなし専用のDevtoolsあり
キャッシュのGCなし(手動管理)gcTimeで自動管理
学習コスト低いやや高い

SWRはデータの読み取りが中心で、ミューテーションが少ないプロジェクトに向いている。ダッシュボード、ブログのフロント、設定画面など。TanStack Queryはミューテーションが多く、キャッシュのライフサイクルを細かく管理したいプロジェクトに向いている。管理画面、ECサイト、フォームの多いアプリなど。

軽量に始めたいならSWR、最初から複雑なキャッシュ戦略が見えているならTanStack Queryという判断でよい。

実践で使うためのヒント

SWRを導入するなら、まずSWRConfigでfetcherとエラーハンドリングのデフォルトをアプリ全体に設定するところから始めるとよい。個別のコンポーネントで毎回fetcherを渡す書き方は、プロジェクトが大きくなるとすぐに面倒になる。

カスタムフックにする習慣もつけておきたい。useSWR('/api/user', fetcher)を直接コンポーネントに書くのではなく、useUserのようなフックに抽出すれば、キーの変更やオプションの追加が一箇所で済む。

function useUser() {
  const { data, error, isLoading } = useSWR('/api/user')
  return {
    user: data,
    isLoading,
    isError: error,
  }
}

TypeScriptを使っているなら、fetcherの返り値の型をジェネリクスで指定できる。

const { data } = useSWR<User>('/api/user')
// data の型は User | undefined

既存プロジェクトへの導入は段階的に進められる。既存のuseEffectによるデータフェッチングを、影響の小さいコンポーネントから1つずつuseSWRに置き換えていけばよい。全体を一度に書き換える必要はない。

react zustand

React向けの非常にシンプルで軽量な状態管理ライブラリです。

Reduxのように複雑な設定が必要なく、「ただのJavaScriptオブジェクトを作る感覚」で扱えます

createで作成したものが「ストア」

ストア、グローバルステートとは

そもそもストアとはグローバルステートを保存するためのオブジェクトや仕組みになります

グローバルステートとは、useStateで管理するような特定のコンポーネント内でのみではなく、アプリのどこからでもアクセスできるデータ

createは、storeを作成して、そのstoreにアクセスするためのフックを返します。

storeの初期状態と更新関数を定義したオブジェクトを記述します

簡単なサンプル

import { create } from 'zustand';

const useStore = create((set) => ({
  // 状態(State)
  count: 0,

  // 更新関数(Action)
  inc: () => set((state) => ({ count: state.count + 1 })),
}));

目次