Next.js App Routerでの実装パターン
Next.js App Routerを使った実装パターンを、実際のコード例とともに詳しく解説します。
Next.js App Routerでの実装パターン
Next.js App Routerを使った実装パターンを、実際のコード例とともに詳しく解説します。サーバーコンポーネントとクライアントコンポーネントの使い分けから、データフェッチング、ルーティングまで、実践的なパターンを紹介します。
Next.js App Routerとは
Next.js App Routerは、Next.js 13以降で導入された新しいルーティングシステムです。ファイルベースのルーティングに加えて、サーバーコンポーネント、ストリーミング、データフェッチングの最適化などの機能を提供します。
App Routerの主な特徴
- サーバーコンポーネント: デフォルトでサーバーサイドでレンダリング
- ファイルベースルーティング:
appディレクトリ内のファイル構造でルーティング - レイアウト: 共通レイアウトを簡単に実装
- ストリーミング: 段階的なデータ読み込みとレンダリング
- データフェッチング: サーバーコンポーネントでの効率的なデータ取得
サーバーコンポーネントとクライアントコンポーネント
サーバーコンポーネント
サーバーコンポーネントは、デフォルトでサーバーサイドでレンダリングされます。インタラクティブな機能(useState、useEffect、イベントハンドラーなど)は使用できませんが、データベースへの直接アクセスや、APIキーの使用などが可能です。
// app/tools/page.tsx(サーバーコンポーネント)
import { getAllToolMetadata } from '@/lib/tools/loadTools';
/**
* ツール一覧ページ(サーバーコンポーネント)
* データフェッチングをサーバーサイドで実行
*/
export default async function ToolsPage() {
// サーバーサイドでデータを取得
const tools = getAllToolMetadata();
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 className="text-4xl font-bold mb-4">ツール一覧</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{tools.map((tool) => (
<ToolCard key={tool.slug} tool={tool} />
))}
</div>
</div>
);
}
クライアントコンポーネント
クライアントコンポーネントは、'use client'ディレクティブを使用して、クライアントサイドでレンダリングされます。インタラクティブな機能(useState、useEffect、イベントハンドラーなど)を使用できます。
// components/tools/ToolCard.tsx(クライアントコンポーネント)
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { ToolMetadata } from '@/lib/tools/types';
interface ToolCardProps {
tool: ToolMetadata;
}
/**
* ツールカードコンポーネント(クライアントコンポーネント)
* インタラクティブな機能を使用
*/
export function ToolCard({ tool }: ToolCardProps) {
const [isHovered, setIsHovered] = useState(false);
return (
<Link
href={`/tools/${tool.slug}`}
className="block"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className={`p-6 bg-white rounded-lg shadow-md transition-all ${
isHovered ? 'shadow-xl scale-105' : ''
}`}
>
<div className="text-4xl mb-4">{tool.icon}</div>
<h3 className="text-xl font-bold mb-2">{tool.name}</h3>
<p className="text-gray-600">{tool.description}</p>
</div>
</Link>
);
}
サーバーコンポーネントとクライアントコンポーネントの使い分け
サーバーコンポーネントを使用する場合:
- データフェッチング
- データベースへのアクセス
- APIキーの使用
- 大きな依存関係の使用(サーバーサイドのみで実行)
クライアントコンポーネントを使用する場合:
- インタラクティブな機能(useState、useEffect)
- イベントハンドラー(onClick、onChangeなど)
- ブラウザ専用API(localStorage、windowなど)
- サードパーティライブラリ(状態管理、アニメーションなど)
データフェッチングパターン
1. サーバーコンポーネントでのデータフェッチング
サーバーコンポーネントでは、async/awaitを使って直接データを取得できます。
// app/blog/[slug]/page.tsx
import { getArticle } from '@/lib/articles/loadArticles';
import { notFound } from 'next/navigation';
interface ArticlePageProps {
params: {
slug: string;
};
}
/**
* 記事詳細ページ(サーバーコンポーネント)
* サーバーサイドでデータを取得
*/
export default async function ArticlePage({ params }: ArticlePageProps) {
const article = getArticle(params.slug);
if (!article) {
notFound();
}
return (
<article className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 className="text-4xl font-bold mb-4">{article.title}</h1>
<div className="prose prose-lg max-w-none">
{/* 記事本文 */}
</div>
</article>
);
}
2. 並列データフェッチング
複数のデータを並列で取得することで、パフォーマンスを向上させます。
// app/tools/[slug]/page.tsx
import { getToolMetadata } from '@/lib/tools/loadTools';
import { getRelatedArticles } from '@/lib/articles/loadArticles';
interface ToolPageProps {
params: {
slug: string;
};
}
/**
* ツール詳細ページ(サーバーコンポーネント)
* 並列でデータを取得
*/
export default async function ToolPage({ params }: ToolPageProps) {
// 並列でデータを取得
const [tool, relatedArticles] = await Promise.all([
getToolMetadata(params.slug),
getRelatedArticles(params.slug, 3),
]);
if (!tool) {
notFound();
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 className="text-4xl font-bold mb-4">{tool.name}</h1>
{/* ツールコンテンツ */}
{/* 関連記事 */}
{relatedArticles.length > 0 && (
<section className="mt-12">
<h2 className="text-2xl font-bold mb-6">関連記事</h2>
{/* 関連記事の表示 */}
</section>
)}
</div>
);
}
3. ストリーミング
段階的なデータ読み込みとレンダリングを実装します。
// app/dashboard/page.tsx
import { Suspense } from 'react';
/**
* ダッシュボードページ(ストリーミング)
*/
export default function DashboardPage() {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 className="text-4xl font-bold mb-8">ダッシュボード</h1>
{/* 高速に読み込めるコンテンツ */}
<QuickStats />
{/* 遅いデータはストリーミング */}
<Suspense fallback={<StatsSkeleton />}>
<SlowStats />
</Suspense>
<Suspense fallback={<ArticlesSkeleton />}>
<RecentArticles />
</Suspense>
</div>
);
}
/**
* 遅いデータの取得(ストリーミング)
*/
async function SlowStats() {
// 遅いデータフェッチング
const stats = await fetchSlowStats();
return <StatsDisplay stats={stats} />;
}
ルーティングパターン
1. 動的ルーティング
[slug]や[id]を使った動的ルーティングを実装します。
// app/blog/[slug]/page.tsx
interface ArticlePageProps {
params: {
slug: string;
};
}
export default async function ArticlePage({ params }: ArticlePageProps) {
const article = getArticle(params.slug);
// ...
}
2. ネストされたルーティング
複数の動的セグメントを使ったネストされたルーティングを実装します。
// app/tools/[category]/[slug]/page.tsx
interface ToolPageProps {
params: {
category: string;
slug: string;
};
}
export default async function ToolPage({ params }: ToolPageProps) {
const { category, slug } = params;
// ...
}
3. ルートグループ
(group)を使ったルートグループで、URLに影響を与えずにレイアウトを整理します。
app/
(marketing)/
about/
contact/
(dashboard)/
dashboard/
settings/
レイアウトパターン
1. 共通レイアウト
app/layout.tsxで共通レイアウトを実装します。
// app/layout.tsx
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import './globals.css';
export const metadata = {
title: 'うつろい房',
description: '物理シミュレーション、デザインツール、ユーティリティツールを提供',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
2. ネストされたレイアウト
特定のルートにのみ適用されるレイアウトを実装します。
// app/tools/layout.tsx
export default function ToolsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="tools-layout">
<aside>
{/* ツール一覧のサイドバー */}
</aside>
<main>{children}</main>
</div>
);
}
メタデータとSEO
generateMetadata関数
動的なメタデータを生成します。
// app/blog/[slug]/page.tsx
import { generateMetadata } from 'next';
export async function generateMetadata({
params,
}: {
params: { slug: string };
}) {
const article = getArticle(params.slug);
if (!article) {
return {
title: '記事が見つかりません',
};
}
return {
title: `${article.title} | うつろい房`,
description: article.description,
openGraph: {
title: article.title,
description: article.description,
type: 'article',
publishedTime: article.publishedAt,
},
};
}
構造化データ(JSON-LD)
SEO対策として構造化データを追加します。
// app/blog/[slug]/page.tsx
export default async function ArticlePage({ params }: ArticlePageProps) {
const article = getArticle(params.slug);
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.title,
description: article.description,
datePublished: article.publishedAt,
dateModified: article.updatedAt,
author: {
'@type': 'Person',
name: 'Tool Platform',
},
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>
{/* 記事コンテンツ */}
</article>
</>
);
}
エラーハンドリング
エラーページ
error.tsxでエラーハンドリングを実装します。
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h2 className="text-2xl font-bold mb-4">エラーが発生しました</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={() => reset()}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
再試行
</button>
</div>
);
}
404ページ
not-found.tsxで404ページを実装します。
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 text-center">
<h1 className="text-4xl font-bold mb-4">404</h1>
<p className="text-gray-600 mb-8">ページが見つかりませんでした</p>
<Link
href="/"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
ホームに戻る
</Link>
</div>
);
}
ローディング状態
ローディングページ
loading.tsxでローディング状態を表示します。
// app/tools/loading.tsx
export default function Loading() {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-48 bg-gray-200 rounded"></div>
))}
</div>
</div>
</div>
);
}
API Routes
API Routeの実装
app/apiディレクトリでAPI Routeを実装します。
// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { writeFileSync } from 'fs';
import { join } from 'path';
/**
* お問い合わせフォームのAPI Route
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { name, email, subject, message } = body;
// バリデーション
if (!name || !email || !subject || !message) {
return NextResponse.json(
{ error: 'すべての項目を入力してください' },
{ status: 400 }
);
}
// ファイルに保存
const data = {
name,
email,
subject,
message,
timestamp: new Date().toISOString(),
};
const filename = `contact-${Date.now()}.json`;
const filepath = join(process.cwd(), 'data', 'contacts', filename);
writeFileSync(filepath, JSON.stringify(data, null, 2));
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: 'サーバーエラーが発生しました' },
{ status: 500 }
);
}
}
実践的な実装例
ツールプラットフォームの実装パターン
// app/tools/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { getToolMetadata } from '@/lib/tools/loadTools';
import { getTool } from '@/lib/tools/toolRegistry';
import { getRelatedArticles } from '@/lib/articles/loadArticles';
import { ToolRunner } from '@/components/tools/ToolRunner';
import { RelatedArticles } from '@/components/blog/RelatedArticles';
interface ToolPageProps {
params: {
slug: string;
};
}
/**
* メタデータを生成
*/
export async function generateMetadata({ params }: ToolPageProps) {
const tool = getToolMetadata(params.slug);
if (!tool) {
return {
title: 'ツールが見つかりません',
};
}
return {
title: `${tool.name} - うつろい房`,
description: tool.description,
};
}
/**
* ツール詳細ページ(サーバーコンポーネント)
*/
export default async function ToolPage({ params }: ToolPageProps) {
// 並列でデータを取得
const [tool, relatedArticles] = await Promise.all([
getToolMetadata(params.slug),
getRelatedArticles(params.slug, 3),
]);
if (!tool) {
notFound();
}
const registryEntry = getTool(params.slug);
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 className="text-4xl font-bold mb-4">{tool.name}</h1>
<p className="text-gray-600 text-lg mb-8">{tool.description}</p>
{/* ツール実行エリア(クライアントコンポーネント) */}
{registryEntry && (
<ToolRunner tool={registryEntry} />
)}
{/* 関連記事(サーバーコンポーネント) */}
{relatedArticles.length > 0 && (
<RelatedArticles articles={relatedArticles} />
)}
</div>
);
}
まとめ
Next.js App Routerを使うことで、以下のような実装パターンが可能になります:
- サーバーコンポーネントとクライアントコンポーネントの使い分け: パフォーマンスとインタラクティブ性のバランス
- 効率的なデータフェッチング: サーバーサイドでのデータ取得、並列フェッチング、ストリーミング
- 柔軟なルーティング: 動的ルーティング、ネストされたルーティング、ルートグループ
- 共通レイアウト: ルートレイアウト、ネストされたレイアウト
- SEO対策: メタデータ生成、構造化データ
- エラーハンドリング: エラーページ、404ページ、ローディング状態
- API Routes: サーバーサイドAPIの実装
これらのパターンを組み合わせることで、高性能で保守性の高いNext.jsアプリケーションを実装できます。
関連記事
関連記事
TypeScriptで物理シミュレーションを実装する方法
TypeScriptを使って物理シミュレーションを実装する方法を、実際のコード例とともに詳しく解説します。
2026年1月8日
Canvas APIを使った描画の最適化
Canvas APIを使った描画を最適化する方法を、実際のコード例とともに詳しく解説します。
2026年1月8日
Matter.jsを使った物理シミュレーション入門
Matter.jsを使った物理シミュレーションの実装方法を、実際のコード例とともに解説します。
2026年1月7日