react-data-fetching について
React アプリでサーバーデータを取得・同期するための、フレームワークに依存しないパターン集です。キャッシュや重複排除などを通じて、読み込み処理の改善を支援します。
主な機能
- 01独立したリクエストの並列化
- 02TanStack Query または SWR の利用
- 03Suspense による読み込み調整
- 04ナビゲーション前のプリフェッチ
- 05サーバーレンダリング時の重複排除
- 06楽観的更新の実装
活用シーン
- 01useEffect 内の fetch の置き換え
- 02React ビューのリクエストウォーターフォール削減
- 03共有データリクエストのキャッシュと重複排除
- 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 データフェッチのガイダンスです。