mcpField
R
react-data-fetching

react-data-fetching

github.com/patternsdev開発2571.1k 回インストール28 週間前に更新
リポジトリ

react-data-fetching について

React アプリでサーバーデータを取得・同期するための、フレームワークに依存しないパターン集です。キャッシュや重複排除などを通じて、読み込み処理の改善を支援します。

主な機能

  1. 01独立したリクエストの並列化
  2. 02TanStack Query または SWR の利用
  3. 03Suspense による読み込み調整
  4. 04ナビゲーション前のプリフェッチ
  5. 05サーバーレンダリング時の重複排除
  6. 06楽観的更新の実装

活用シーン

  1. 01useEffect 内の fetch の置き換え
  2. 02React ビューのリクエストウォーターフォール削減
  3. 03共有データリクエストのキャッシュと重複排除
  4. 04読み込みとデータ変更時の応答改善

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/patternsdev/skills --skill react-data-fetching

インストール後、Claude Code で/react-data-fetchingを入力して読み込まれたか確認します。

詳細

React のデータ取得パターン

目次

React アプリケーションでサーバーデータを取得、キャッシュ、同期するための、本番環境で使えるパターンです。これらのパターンはフレームワークに依存しないため、Vite + React Router、Next.js、Remix、独自のセットアップのいずれを使用していても利用できます。

使用するタイミング

次のような場合に、これらのパターンを参考にしてください。

  • コンポーネントにデータ取得を追加するとき
  • useEffect + fetch を適切なデータレイヤーに置き換えるとき
  • キャッシュ、重複排除、楽観的更新を実装するとき
  • ウォーターフォール型の読み込みパターンをデバッグするとき
  • データ取得ライブラリを選ぶとき

手順

  • コード生成、レビュー、リファクタリングの際に、これらのパターンを適用してください。キャッシュや重複排除を行わずに fetch を effect 内で実行している箇所を見つけたら、適切なパターンを提案してください。

詳細

概要

React アプリで最もよくあるパフォーマンス上の問題は、リクエストのウォーターフォールです。これは、並行して実行できる取得処理を順番に実行することを指します。2 番目によくある問題は、冗長な取得です。これは、複数のコンポーネントが同じデータをそれぞれ個別に取得することを指します。以下のパターンでは、効果の大きい修正から順に、この両方の問題に対処します。


1. 独立した取得処理を Promise.all で並列化する

効果:重大 — 順次実行のウォーターフォールを解消し、2~10 倍の改善につながります。

複数の取得処理に相互依存がない場合は、並行して実行します。

避ける — 順次実行(3 回のラウンドトリップ):

async function loadDashboard() {
  const user = await fetchUser()
  const posts = await fetchPosts()
  const notifications = await fetchNotifications()
  return { user, posts, notifications }
}

推奨 — 並列実行(1 回のラウンドトリップ):

async function loadDashboard() {
  const [user, posts, notifications] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchNotifications(),
  ])
  return { user, posts, notifications }
}

取得処理に部分的な依存関係がある場合(B は A に依存するが、C は依存しない場合)は、独立した処理をすぐに開始します。

async function loadPage() {
  const userPromise = fetchUser()
  const configPromise = fetchConfig()

  const user = await userPromise
  const [config, posts] = await Promise.all([
    configPromise,
    fetchPosts(user.id), // depends on user
  ])
  return { user, config, posts }
}

2. 値が必要になるまで await を遅らせる

効果:高 — まだ必要のない結果を待たずに、処理を早く開始できます。

よくある間違いは、後続のコードですぐに結果を必要としない場合でも、各 promise をただちに await することです。promise を早めに開始し、実際に値を読み取る箇所で await します。

避ける — 不必要に処理をブロックする:

async function loadProfile(userId: string) {
  const user = await fetchUser(userId)       // waits here
  const prefs = await fetchPreferences()     // starts only after user resolves
  const avatar = buildAvatarUrl(user.avatar)
  return { user, prefs, avatar }
}

推奨 — 早く開始し、await は後で行う:

async function loadProfile(userId: string) {
  const userPromise = fetchUser(userId)      // starts immediately
  const prefsPromise = fetchPreferences()    // starts immediately

  const user = await userPromise             // await when needed
  const avatar = buildAvatarUrl(user.avatar)
  const prefs = await prefsPromise           // may already be resolved

  return { user, prefs, avatar }
}

これは Promise.all と併用できます。取得処理の間で中間結果が必要な場合は await を遅らせ、すべての処理をまとめて待てる場合は Promise.all を使います。


3. クライアント側のデータには TanStack Query を使う

効果:重大 — キャッシュ、重複排除、再検証、エラー処理を自動化します。

素の useEffect + fetch には、キャッシュ、重複排除、再試行、バックグラウンド更新がありません。データ取得ライブラリを使いましょう。

避ける — キャッシュなし、重複排除なし、エラー処理なし:

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    setLoading(true)
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser)
      .finally(() => setLoading(false))
  }, [userId])

  if (loading) return <Skeleton />
  return <div>{user?.name}</div>
}

推奨 — TanStack Query(Vite + React アプリに推奨):

import { useQuery } from '@tanstack/react-query'

function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  })

  if (isLoading) return <Skeleton />
  return <div>{user?.name}</div>
}

TanStack Query は Vite アプリに最適な選択肢です。フレームワークに依存せず、組み込みの useSuspenseQuery、開発ツール、無限クエリ、楽観的ミューテーション、オフラインサポートを備えています。SWR はより軽量な代替手段で、基本機能(重複排除、キャッシュ、再検証)を備えていますが、複雑なミューテーションのワークフローに対応する機能は少なめです。

どちらも、リクエストの重複排除、stale-while-revalidate キャッシュ、自動再試行、バックグラウンド更新を提供します。

Vite アプリでのセットアップ:

// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000, // 1 minute
      retry: 2,
    },
  },
})

createRoot(document.getElementById('root')!).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
)

4. 宣言的な読み込み状態には Suspense を使う

効果:高 — コードがすっきりし、読み込みを自動的に調整でき、ストリーミングをサポートします。

Suspense を使うと、各コンポーネントで isLoading の状態を管理する代わりに、コンポーネントツリー内に読み込み境界を宣言できます。

避ける — 手動で読み込みを調整する:

function Dashboard() {
  const { data: user, isLoading: userLoading } = useQuery(userQuery)
  const { data: stats, isLoading: statsLoading } = useQuery(statsQuery)

  if (userLoading || statsLoading) return <FullPageSpinner />
  return (
    <div>
      <UserHeader user={user} />
      <StatsPanel stats={stats} />
    </div>
  )
}

推奨 — Suspense 境界:

function Dashboard() {
  return (
    <Suspense fallback={<FullPageSpinner />}>
      <DashboardContent />
    </Suspense>
  )
}

function DashboardContent() {
  const { data: user } = useSuspenseQuery(userQuery)
  const { data: stats } = useSuspenseQuery(statsQuery)
  return (
    <div>
      <UserHeader user={user} />
      <StatsPanel stats={stats} />
    </div>
  )
}

独立したセクションには個別の Suspense 境界を使い、それぞれ独立して読み込めるようにします。

function Dashboard() {
  return (
    <div>
      <Suspense fallback={<HeaderSkeleton />}>
        <UserHeader />
      </Suspense>
      <Suspense fallback={<StatsSkeleton />}>
        <StatsPanel />
      </Suspense>
    </div>
  )
}

TanStack Query は useSuspenseQuery を提供し、SWR は { suspense: true } オプションを提供します。


5. ナビゲーション前にデータをプリフェッチする

効果:高 — ページ遷移時の読み込み状態を解消します。

ユーザーがナビゲーションを確定する前に、ホバー、フォーカス、またはルートのプリロードでデータ取得を開始します。

TanStack Query を使う場合:

import { useQueryClient } from '@tanstack/react-query'

function ProjectLink({ projectId }: { projectId: string }) {
  const queryClient = useQueryClient()

  const prefetch = () => {
    queryClient.prefetchQuery({
      queryKey: ['project', projectId],
      queryFn: () => fetchProject(projectId),
      staleTime: 30_000,
    })
  }

  return (
    <Link
      to={`/projects/${projectId}`}
      onMouseEnter={prefetch}
      onFocus={prefetch}
    >
      View Project
    </Link>
  )
}

React Router の loader を使う場合(Vite アプリ):

// routes.tsx
const routes = [
  {
    path: '/projects/:id',
    loader: ({ params }) => queryClient.ensureQueryData({
      queryKey: ['project', params.id],
      queryFn: () => fetchProject(params.id!),
    }),
    Component: ProjectPage,
  },
]

6. サーバーサイドの重複排除に React.cache() を使用する

影響:中 — 1 回のサーバーレンダリング内で、コストの高い処理の重複を排除します。

サーバーコンポーネント(RSC)では、React.cache() により、複数のコンポーネントから呼び出された同じ非同期関数が、リクエストごとに 1 回だけ実行されます。

import { cache } from 'react'

export const getSession = cache(async () => {
  const session = await auth()
  if (!session?.user?.id) return null
  return session
})

export const getUser = cache(async (userId: string) => {
  return await db.user.findUnique({ where: { id: userId } })
})

同じレンダリング内で複数のコンポーネントが getSession() を呼び出しても、1 回の実行が共有されます。

重要: キャッシュキーにはプリミティブ型の引数(文字列、数値)を使用してください。インラインオブジェクトは新しい参照を作成するため、キャッシュミスの原因になります。

// Cache miss every time — new object reference
getUser({ id: '123' })
getUser({ id: '123' }) // miss

// Cache hit — same string value
getUser('123')
getUser('123') // hit

7. 即座にフィードバックを返すため、楽観的更新を実装する

影響:高 — サーバーの応答を待たずに、UI がすぐに反応します。

結果を予測できる変更(「いいね」の切り替え、名前の更新など)では、UI を即座に更新し、サーバーの応答と整合させます。

TanStack Query を使用する場合:

import { useMutation, useQueryClient } from '@tanstack/react-query'

function LikeButton({ postId }: { postId: string }) {
  const queryClient = useQueryClient()

  const { mutate: toggleLike } = useMutation({
    mutationFn: () => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),
    onMutate: async () => {
      await queryClient.cancelQueries({ queryKey: ['post', postId] })
      const previous = queryClient.getQueryData<Post>(['post', postId])
      queryClient.setQueryData<Post>(['post', postId], old => ({
        ...old!,
        liked: !old!.liked,
        likeCount: old!.liked ? old!.likeCount - 1 : old!.likeCount + 1,
      }))
      return { previous }
    },
    onError: (_err, _vars, context) => {
      queryClient.setQueryData(['post', postId], context?.previous)
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['post', postId] })
    },
  })

  return <button onClick={() => toggleLike()}>Like</button>
}

8. コンポーネントツリーでのフェッチのウォーターフォールを避ける

影響:重大 — 親コンポーネントのフェッチ完了後に子コンポーネントがフェッチすることは、パフォーマンス問題の第 1 位です。

親コンポーネントがデータをフェッチし、その結果に基づいて子コンポーネントが自身のデータをフェッチすると、ウォーターフォールが発生します。並列にフェッチするように構成を変更してください。

避ける — 親コンポーネントの処理が終わるまで、子コンポーネントは開始できません:

function UserPage({ userId }: { userId: string }) {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })

  if (!user) return <Skeleton />
  return <UserPosts userId={user.id} /> // starts fetching only after user loads
}

function UserPosts({ userId }: { userId: string }) {
  const { data: posts } = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId),
  })
  // ...
}

推奨 — 同じ階層で両方をフェッチします:

function UserPage({ userId }: { userId: string }) {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })
  const { data: posts } = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId),
  })

  if (!user) return <Skeleton />
  return (
    <div>
      <UserHeader user={user} />
      <PostList posts={posts ?? []} />
    </div>
  )
}

または、ルートレベルのローダーを使って、コンポーネントのレンダリング前にすべてのデータをフェッチします。


9. グローバルイベントリスナーの重複を排除する

影響:中 — コンポーネントのインスタンス数に応じてリスナーが増えるのを防ぎます。

複数のコンポーネントインスタンスが同じグローバルイベント(リサイズ、スクロール、オンラインなど)を必要とする場合は、単一のリスナーを共有します。

// hooks/useOnlineStatus.ts
import { useSyncExternalStore } from 'react'

function subscribe(callback: () => void) {
  window.addEventListener('online', callback)
  window.addEventListener('offline', callback)
  return () => {
    window.removeEventListener('online', callback)
    window.removeEventListener('offline', callback)
  }
}

function getSnapshot() {
  return navigator.onLine
}

export function useOnlineStatus() {
  return useSyncExternalStore(subscribe, getSnapshot, () => true)
}

useSyncExternalStore はサブスクリプションの重複を自動的に排除し、同時レンダリング間で一貫した状態を保証します。


10. スクロールとタッチにはパッシブイベントリスナーを使用する

影響:低〜中 — リスナーによるスクロールのカクつきを防ぎます。

パッシブでないスクロール/タッチリスナーは、ブラウザーのコンポジタースレッドをブロックします。preventDefault() を呼び出さない場合は、パッシブとして指定してください。

避ける — スクロールをブロックします:

useEffect(() => {
  const handler = () => trackScroll(window.scrollY)
  window.addEventListener('scroll', handler)
  return () => window.removeEventListener('scroll', handler)
}, [])

推奨 — ブロックしません:

useEffect(() => {
  const handler = () => trackScroll(window.scrollY)
  window.addEventListener('scroll', handler, { passive: true })
  return () => window.removeEventListener('scroll', handler)
}, [])

11. クライアントストレージにスキーマバージョンを設定する

影響:低〜中 — 古い localStorage データが原因でクラッシュするのを防ぎます。

localStorage または sessionStorage からデータを読み込む際、以前のアプリバージョンの古いデータが原因でアプリがクラッシュすることがあります。スキーマバージョンを追加し、検証してください。

避ける — スキーマ変更時にクラッシュします:

const [prefs, setPrefs] = useState(() => {
  return JSON.parse(localStorage.getItem('prefs') || '{}')
})

推奨 — バージョン管理とフォールバックを使用します:

const PREFS_VERSION = 2

const [prefs, setPrefs] = useState<Prefs>(() => {
  try {
    const raw = localStorage.getItem('prefs')
    if (!raw) return DEFAULT_PREFS
    const parsed = JSON.parse(raw)
    if (parsed._v !== PREFS_VERSION) return DEFAULT_PREFS
    return parsed
  } catch {
    return DEFAULT_PREFS
  }
})

// On save, include version
useEffect(() => {
  localStorage.setItem('prefs', JSON.stringify({ ...prefs, _v: PREFS_VERSION }))
}, [prefs])

ソース

patterns.dev のパターン — より広範なウェブエンジニアリングコミュニティに向けた、フレームワークに依存しない React データフェッチのガイダンスです。

コメント