react-data-fetching
TanStack Query・SWR・Suspenseを活用したモダンなReactのデータフェッチングパターンを習得できます。Reactアプリケーションでキャッシュ制御・リクエスト重複排除・楽観的更新・並列ローディングを実装する際にご活用ください。
description の原文を見る
Teaches modern React data fetching patterns with TanStack Query, SWR, and Suspense. Use when implementing caching, deduplication, optimistic updates, or parallel loading in React applications.
SKILL.md 本文
React データフェッチングパターン
目次
React アプリケーションでサーバーデータのフェッチ、キャッシング、同期を行うためのプロダクションレディなパターン。これらのパターンはフレームワークに依存しません。Vite + React Router、Next.js、Remix、またはカスタムセットアップなど、どのセットアップでも機能します。
使用する場面
以下の場面でこれらのパターンを参照してください:
- コンポーネントにデータフェッチングを追加する
useEffect+fetchを適切なデータレイヤーに置き換える- キャッシング、重複排除、楽観的な更新を実装する
- ウォーターフォールローディングパターンをデバッグする
- データフェッチングライブラリの選択を行う
手順
- コード生成、レビュー、リファクタリング中にこれらのパターンを適用してください。キャッシングや重複排除がなくエフェクト内でフェッチするのを見かけたら、適切なパターンを提案してください。
詳細
概要
React アプリケーションで最も一般的なパフォーマンス問題は、リクエストウォーターフォールです。順序立てて実行される複数のフェッチが並列に実行できる場合です。次に一般的な問題は、冗長なフェッチです。複数のコンポーネントが独立して同じデータをフェッチする場合です。以下のパターンはこの両方に対応しており、最も影響の大きい修正から始まります。
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), // user に依存
])
return { user, config, posts }
}
2. 値が必要な時まで await を遅延させる
影響:高 — 結果が不要な値をブロックすることなく、作業をより早く開始します。
一般的なミスは、後のコードでその結果が不要な場合でも、各 promise をすぐに await することです。promise を早く開始してから、実際に値を読む時点で await してください。
非推奨 — 不必要にブロック:
async function loadProfile(userId: string) {
const user = await fetchUser(userId) // ここで待つ
const prefs = await fetchPreferences() // user が解決した後にのみ開始
const avatar = buildAvatarUrl(user.avatar)
return { user, prefs, avatar }
}
推奨 — 早期開始、遅延 await:
async function loadProfile(userId: string) {
const userPromise = fetchUser(userId) // すぐに開始
const prefsPromise = fetchPreferences() // すぐに開始
const user = await userPromise // 必要なときに await
const avatar = buildAvatarUrl(user.avatar)
const prefs = await prefsPromise // すでに解決しているかもしれない
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分
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 ローダー使用(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() を使用する
影響:中 — 単一のサーバーレンダリング内で高価な操作を重複排除します。
サーバーコンポーネント(RSC)では、複数のコンポーネントで同じ async 呼び出しが1回のリクエストにつき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つの実行を共有します。
重要: キャッシュキーにはプリミティブ引数(文字列、数字)を使用してください。インラインオブジェクトは新しい参照を作成し、キャッシュ漏れを引き起こします:
// 毎回キャッシュ漏れ — 新しいオブジェクト参照
getUser({ id: '123' })
getUser({ id: '123' }) // 漏れ
// キャッシュ命中 — 同じ文字列値
getUser('123')
getUser('123') // 命中
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} /> // user ロード後にのみフェッチ開始
}
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. グローバルイベントリスナーの重複を排除する
影響:中 — N コンポーネントインスタンスの N リスナーを防止します。
複数のコンポーネントインスタンスが同じグローバルイベント(resize、scroll、online)を必要とする場合、単一のリスナーを共有してください。
// 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. クライアントストレージのスキーマバージョン
影響:低~中 — stale localStorage データからのクラッシュを防止します。
localStorage または sessionStorage から読み込む場合、前のアプリバージョンの stale データはアプリをクラッシュさせる可能性があります。スキーマバージョンを追加して検証してください。
非推奨 — スキーマ変更でクラッシュ:
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
}
})
// 保存時、バージョンを含める
useEffect(() => {
localStorage.setItem('prefs', JSON.stringify({ ...prefs, _v: PREFS_VERSION }))
}, [prefs])
出典
patterns.dev からのパターン — より広いウェブエンジニアリングコミュニティ向けのフレームワークに依存しない React データフェッチングガイダンス。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- patternsdev
- リポジトリ
- patternsdev/skills
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/patternsdev/skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。