nextjs-data-fetching について
Next.js App Routerでデータ取得を実装する開発者向けのスキルです。サーバー・クライアントでの取得、キャッシュ、再検証、ローディングとエラー状態、Server Actionsを扱います。
主な機能
- 01Server Componentsでのデータ取得
- 02独立したリクエストの並列化
- 03キャッシュと再検証の設定
- 04SWRまたはReact Queryとの連携
- 05ローディングとエラー状態の処理
- 06Server Actionsによる変更処理
活用シーン
- 01定期的にデータを更新するページの構築
- 02ダッシュボードデータの並列読み込み
- 03クライアント側キャッシュと更新の追加
- 04フォーム変更とキャッシュ再検証の実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ giuseppe-trisciuoglio/ developer-kit --skill nextjs-data-fetchingインストール後、Claude Code で/nextjs-data-fetchingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/caching-strategies.mdclient-fetching.mddata-fetching.mderror-boundaries.mderror-loading-states.mdreact-query.mdserver-actions.md詳細
Next.js のデータ取得
概要
Next.js App Router でデータを取得するためのパターンを提供します。サーバーサイドでのデータ取得、SWR/React Query との統合、ISR、再検証、エラーバウンダリー、ローディング状態を扱います。
使用する場面
- Next.js App Router でデータ取得を実装する場合
- Server Components と Client Components のどちらを使うか選択する場合
- クライアントサイドのキャッシュに SWR または React Query を設定する場合
- ISR、時間ベースの再検証、またはオンデマンド再検証を設定する場合
- ローディング状態とエラー状態を処理する場合
- Server Actions でフォームを構築する場合
手順
Server Component でのデータ取得
非同期 Server Components 内で直接データを取得します。
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
並列データ取得
互いに独立したリクエストには Promise.all() を使用します。
async function getDashboardData() {
const [user, posts, analytics] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/analytics').then(r => r.json()),
]);
return { user, posts, analytics };
}
export default async function DashboardPage() {
const { user, posts, analytics } = await getDashboardData();
// Render dashboard
}
順次データ取得(依存関係がある場合)
async function getUserPosts(userId: string) {
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
return { user, posts };
}
時間ベースの再検証(ISR)
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 } // Revalidate every 60 seconds
});
return res.json();
}
オンデマンド再検証
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const tag = request.nextUrl.searchParams.get('tag');
if (tag) {
revalidateTag(tag);
return Response.json({ revalidated: true });
}
return Response.json({ revalidated: false }, { status: 400 });
}
選択的に再検証できるよう、データにタグを付けます。
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'], revalidate: 3600 }
});
return res.json();
}
キャッシュを無効にする
async function getRealTimeData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
});
return res.json();
}
// Or:
export const dynamic = 'force-dynamic';
クライアントサイドでのデータ取得
SWR との統合
インストール:npm install swr
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export function Posts() {
const { data, error, isLoading } = useSWR('/api/posts', fetcher, {
refreshInterval: 5000,
revalidateOnFocus: true,
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Failed to load posts</div>;
return (
<ul>
{data.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
React Query との統合
インストール:npm install @tanstack/react-query
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
},
},
}));
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
ミューテーション、楽観的更新、無限クエリ、高度なパターンについては、react-query.md を参照してください。
エラーバウンダリー
クライアントサイドでのデータ取得をエラーバウンダリーで囲み、失敗を適切に処理します。
ErrorBoundary の完全な実装(基本形、リセットコールバック付き)と、データ取得での使用例については、error-boundaries.md を参照してください。
Server Actions
キャッシュ再検証を伴うミューテーションには、Server Actions を使用します。
useActionState によるフォームの検証、エラー処理、キャッシュの無効化を含む完全な例については、server-actions.md を参照してください。
ローディング状態
loading.tsx パターン
// app/posts/loading.tsx
export default function PostsLoading() {
return (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-gray-200 animate-pulse rounded" />
))}
</div>
);
}
Suspense バウンダリー
// app/posts/page.tsx
import { Suspense } from 'react';
import { PostsList } from './PostsList';
import { PostsSkeleton } from './PostsSkeleton';
export default function PostsPage() {
return (
<div>
<h1>Posts</h1>
<Suspense fallback={<PostsSkeleton />}>
<PostsList />
</Suspense>
</div>
);
}
ベストプラクティス
- Server Components をデフォルトにする — パフォーマンスを高めるため、Server Components でデータを取得します
- 並列データ取得を使う — 互いに独立したリクエストには
Promise.all()を使用して、レイテンシを削減します - 適切なキャッシュを選ぶ:
- 静的データ:再検証間隔を長くします
- 動的データ:再検証間隔を短くするか、
cache: 'no-store'を使います - ユーザー固有のデータ:動的レンダリングを使います
- エラーを適切に処理する — クライアントデータの取得をエラーバウンダリーで囲みます
- ローディング状態を実装する —
loading.tsxまたは Suspense バウンダリーを使います - 次の場合は SWR/React Query を優先する:リアルタイムデータ、ユーザー操作、バックグラウンド更新
- 次の場合は Server Actions を使う:フォーム送信、キャッシュ再検証が必要なミューテーション
制約と警告
重大な制約
- Server Components ではフック(
useState、useEffect)やクライアントデータ取得ライブラリを使用できません - Client Components には
'use client'ディレクティブを含める必要があります - Next.js の
fetchAPI は、標準の Web fetch に Next.js 固有のキャッシュオプションを追加したものです - Server Actions には
'use server'が必要で、Client Components またはフォームアクションからのみ呼び出せます
よくある落とし穴
- ループ内でのデータ取得 — Server Components で順次データを取得するのは避け、並列データ取得を使います
- キャッシュの汚染 — ユーザー固有またはパーソナライズされたデータに
force-cacheを使用しないでください - メモリーリーク — リアルタイムデータを扱う Client Components では、サブスクリプションをクリーンアップします
- ハイドレーションの不一致 — React Query のハイドレーションを使用する場合、サーバーとクライアントで同じ初期状態がレンダリングされるようにします
例
例 1:ISR を使ったブログ
入力: 投稿を取得し、1 時間ごとに更新するブログページを作成します。
// app/blog/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
});
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<main>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</main>
);
}
出力: ページはビルド時に静的生成され、1 時間ごとに再検証されます。
例 2:並列データ取得を使ったダッシュボード
入力: ユーザープロフィール、統計情報、最近のアクティビティを並列に表示するダッシュボードを構築します。
// app/dashboard/page.tsx
async function getDashboardData() {
const [user, stats, activity] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/stats').then(r => r.json()),
fetch('/api/activity').then(r => r.json()),
]);
return { user, stats, activity };
}
export default async function DashboardPage() {
const { user, stats, activity } = await getDashboardData();
return (
<div className="dashboard">
<UserProfile user={user} />
<StatsCards stats={stats} />
<ActivityFeed activity={activity} />
</div>
);
}
出力: 3 つのリクエストがすべて同時に実行され、合計読み込み時間が最小限に抑えられます。