swr の使い方

React Hooks library for remote data fetching

v2.4.1/週MIT状態管理
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

SWR の使い方 — React データフェッチングの定番ライブラリ

一言でいうと

SWR は、Vercel チームが開発した React Hooks ベースのデータフェッチングライブラリです。「stale-while-revalidate」戦略(キャッシュを先に返し、裏で再検証する)により、高速かつ常に最新のUIを実現します。

どんな時に使う?

  • REST API からのデータ取得を簡潔に書きたい時useEffect + useState の定型コードを排除し、ローディング・エラー・データの状態管理を1つのフックに集約できます
  • リアルタイム性の高いダッシュボードやSNSフィードを作る時 — フォーカス復帰時の自動再検証、ポーリング、ネットワーク復帰時の再取得などが組み込みで提供されます
  • 楽観的UI更新(Optimistic UI)を実装したい時 — ローカルミューテーションにより、サーバー応答を待たずにUIを即座に更新し、ユーザー体験を向上させられます

インストール

# npm
npm install swr

# yarn
yarn add swr

# pnpm
pnpm add swr

React 16.11.0 以上が必要です。

SWR の基本的な使い方

import useSWR from 'swr'

// fetcher は key(URL)を受け取り、データを返す非同期関数
const fetcher = (url: string): Promise<any> =>
  fetch(url).then((res) => res.json())

type User = {
  id: number
  name: string
  email: string
}

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

  if (error) return <div>エラーが発生しました</div>
  if (isLoading) return <div>読み込み中...</div>

  return (
    <div>
      <h1>{data?.name}</h1>
      <p>{data?.email}</p>
    </div>
  )
}

useSWRkey(リクエストの一意識別子、通常はURL)と fetcher(データ取得関数)を受け取ります。同じ key を持つ複数のコンポーネントがあっても、リクエストは自動的に重複排除されます。

よく使う API

1. useSWR — 基本のデータフェッチング

import useSWR, { SWRConfiguration } from 'swr'

type Repository = {
  stargazers_count: number
  forks_count: number
}

const options: SWRConfiguration = {
  revalidateOnFocus: true,       // フォーカス時に再検証(デフォルト: true)
  revalidateOnReconnect: true,   // ネットワーク復帰時に再検証
  refreshInterval: 0,            // ポーリング間隔(ms)。0で無効
  dedupingInterval: 2000,        // 重複排除の間隔(ms)
  errorRetryCount: 3,            // エラー時のリトライ回数
}

function RepoInfo() {
  const { data, error, isLoading, isValidating, mutate } = useSWR<Repository>(
    'https://api.github.com/repos/vercel/swr',
    fetcher,
    options
  )

  return (
    <div>
      {isValidating && <span>更新中...</span>}
      <p>⭐ {data?.stargazers_count}</p>
      <button onClick={() => mutate()}>手動で再取得</button>
    </div>
  )
}

返り値の主要プロパティ:

プロパティ説明
dataT | undefinedフェッチされたデータ
erroranyfetcher がスローしたエラー
isLoadingboolean初回ロード中(キャッシュなし & リクエスト中)
isValidatingbooleanリクエスト中(初回・再検証問わず)
mutatefunctionキャッシュを更新する関数

2. mutate — ローカルミューテーション(楽観的更新)

import useSWR from 'swr'

type Todo = {
  id: number
  title: string
  completed: boolean
}

function TodoItem({ id }: { id: number }) {
  const { data, mutate } = useSWR<Todo>(`/api/todos/${id}`, fetcher)

  const toggleComplete = async () => {
    const updated = { ...data!, completed: !data!.completed }

    // 楽観的更新: サーバー応答を待たずにUIを即座に更新
    await mutate(
      async () => {
        const res = await fetch(`/api/todos/${id}`, {
          method: 'PATCH',
          body: JSON.stringify({ completed: updated.completed }),
          headers: { 'Content-Type': 'application/json' },
        })
        return res.json()
      },
      {
        optimisticData: updated,  // 即座にUIに反映するデータ
        rollbackOnError: true,    // エラー時にキャッシュを元に戻す
        revalidate: false,        // ミューテーション後の再検証をスキップ
      }
    )
  }

  return (
    <label>
      <input
        type="checkbox"
        checked={data?.completed ?? false}
        onChange={toggleComplete}
      />
      {data?.title}
    </label>
  )
}

3. useSWRInfinite — ページネーション / 無限スクロール

import useSWRInfinite from 'swr/infinite'

type Issue = {
  id: number
  title: string
}

const PAGE_SIZE = 10

function Issues() {
  const getKey = (pageIndex: number, previousPageData: Issue[] | null) => {
    // 最後のページに到達した場合は null を返してフェッチを停止
    if (previousPageData && previousPageData.length === 0) return null
    return `/api/issues?page=${pageIndex + 1}&limit=${PAGE_SIZE}`
  }

  const { data, size, setSize, isLoading, isValidating } =
    useSWRInfinite<Issue[]>(getKey, fetcher)

  // 全ページのデータをフラットに結合
  const issues = data ? data.flat() : []
  const isReachingEnd = data && data[data.length - 1]?.length < PAGE_SIZE

  return (
    <div>
      {issues.map((issue) => (
        <div key={issue.id}>{issue.title}</div>
      ))}
      {!isReachingEnd && (
        <button
          onClick={() => setSize(size + 1)}
          disabled={isValidating}
        >
          {isValidating ? '読み込み中...' : 'もっと読み込む'}
        </button>
      )}
    </div>
  )
}

4. SWRConfig — グローバル設定

import { SWRConfig } from 'swr'

const globalFetcher = (url: string) =>
  fetch(url).then((res) => {
    if (!res.ok) throw new Error('API Error')
    return res.json()
  })

function App() {
  return (
    <SWRConfig
      value={{
        fetcher: globalFetcher,           // 全フックで共通の fetcher
        refreshInterval: 30000,           // 30秒ごとにポーリング
        revalidateOnFocus: true,
        shouldRetryOnError: true,
        errorRetryInterval: 5000,
        onError: (error, key) => {
          console.error(`SWR Error [${key}]:`, error)
        },
      }}
    >
      <Dashboard />
    </SWRConfig>
  )
}

// SWRConfig 配下では fetcher を省略可能
function Dashboard() {
  const { data } = useSWR<{ revenue: number }>('/api/stats')
  return <div>売上: {data?.revenue}</div>
}

5. useSWRMutation — 手動トリガーのミューテーション

import useSWRMutation from 'swr/mutation'

type CreateUserPayload = {
  name: string
  email: string
}

type User = {
  id: number
  name: string
  email: string
}

// 第2引数の arg にトリガー時に渡したデータが入る
async function createUser(url: string, { arg }: { arg: CreateUserPayload }): Promise<User> {
  const res = await fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg),
    headers: { 'Content-Type': 'application/json' },
  })
  return res.json()
}

function CreateUserForm() {
  const { trigger, isMutating, error } = useSWRMutation<User, Error, string, CreateUserPayload>(
    '/api/users',
    createUser
  )

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)

    try {
      const newUser = await trigger({
        name: formData.get('name') as string,
        email: formData.get('email') as string,
      })
      console.log('作成されたユーザー:', newUser)
    } catch (err) {
      // エラーハンドリング
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="名前" required />
      <input name="email" type="email" placeholder="メール" required />
      <button type="submit" disabled={isMutating}>
        {isMutating ? '送信中...' : 'ユーザー作成'}
      </button>
      {error && <p>エラー: {error.message}</p>}
    </form>
  )
}

類似パッケージとの比較

特徴SWRTanStack Query (React Query)Apollo Client
バンドルサイズ (minzip)~4.5 KB~13 KB~33 KB
プロトコル任意(REST, GraphQL 等)任意(REST, GraphQL 等)GraphQL 特化
キャッシュ戦略stale-while-revalidatestale-while-revalidate正規化キャッシュ
DevToolsなし(公式)充実充実
ページネーションuseSWRInfiniteuseInfiniteQueryfetchMore
楽観的更新
SSR/SSG サポート
ミューテーション管理useSWRMutationuseMutation(より高機能)useMutation
学習コスト低いやや高い高い
依存関係React のみReact のみGraphQL エコシステム

選定の目安:

  • SWR — シンプルさ・軽量さを重視。Next.js プロジェクトとの親和性が高い
  • TanStack Query — 複雑なキャッシュ管理、DevTools、ミューテーション管理が必要な場合
  • Apollo Client — GraphQL API を使う場合

注意点・Tips

条件付きフェッチ

key に nullfalsy な値を渡すとフェッチをスキップできます。認証状態に応じたデータ取得などに便利です。

// userId が存在しない場合はフェッチしない
const { data } = useSWR<User>(userId ? `/api/users/${userId}` : null, fetcher)

fetcher でのエラーハンドリング

fetch API はネットワークエラー以外(4xx, 5xx)では例外をスローしません。fetcher 内で明示的にエラーを投げる必要があります。

const fetcher = async (url: string) => {
  const res = await fetch(url)
  if (!res.ok) {
    const error = new Error('API request failed')
    throw error
  }
  return res.json()
}

key のシリアライゼーション

配列やオブジェクトを key に使えます。SWR は内部で安定的にシリアライズします。

// クエリパラメータを含む key
const { data } = useSWR(['/api/users', { page: 1, limit: 10 }], ([url, params]) => {
  const query = new URLSearchParams(params as Record<string, string>).toString()
  return fetcher(`${url}?${query}`)
})

不要な再レンダリングの抑制

返り値の中で使わないフィールドがある場合、compare オプションや返り値の選択で再レンダリングを最適化できます。

// isValidating の変化では再レンダリングしない
const { data } = useSWR<User>('/api/user', fetcher, {
  revalidateOnFocus: false,
})

React Suspense との統合

import { Suspense } from 'react'
import useSWR from 'swr'

function Profile() {
  // suspense: true でデータが準備できるまで Suspense boundary に委譲
  const { data } = useSWR<User>('/api/user', fetcher, { suspense: true })

  // data は必ず存在する(undefined にならない)
  return <div>{data.name}</div>
}

function App() {
  return (
    <Suspense fallback={<div>読み込み中...</div>}>
      <Profile />
    </Suspense>
  )
}

プリフェッチ

ユーザーの操作を先読みして

比較記事