Next.js入門
この章では、Reactのフルスタックフレームワークである Next.js について学びます。
この章で学ぶこと
- Next.jsの特徴とレンダリング戦略(SSR/SSG/CSR)
- App RouterとServer Components
- Server ActionsとRoute Handlers
- ViteプロジェクトとNext.jsの使い分け
この章ではNext.jsの新規プロジェクトを作成します。03章のプロジェクトとは別に、Next.js用のディレクトリで作業します。
Next.jsとは
Next.jsは、Vercel社が開発したReactベースのフルスタックフレームワークです。React公式ドキュメントでも推奨されている、本番環境向けのReactフレームワークです。
主な特徴
| 特徴 | 説明 |
|---|---|
| ハイブリッドレンダリング | SSR、SSG、CSRを柔軟に選択可能 |
| ファイルベースルーティング | ファイル構造がそのままルートに |
| Route Handlers | バックエンドAPIも同一プロジェクトで構築 |
| 自動最適化 | 画像、フォント、スクリプトの自動最適化 |
| Server Components | ReactのServer Componentsをネイティブサポート |
レンダリング戦略
CSR(Client-Side Rendering)
ブラウザでJavaScriptを実行してUIを生成します。本書で学んできたVite + Reactアプリはこの方式です。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Browser │ ───> │ Server │ ───> │ Browser │
│ Request │ │ HTML/JS │ │ Render │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
空のHTML + JavaScriptで
大きなJS UIを構築
メリット: リッチなインタラクション デメリット: 初期表示が遅い、SEOに不利。
SSR(Server-Side Rendering)
サーバーでHTMLを生成してブラウザに送信します。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Browser │ ───> │ Server │ ───> │ Browser │
│ Request │ │ Generate │ │ Display │
└─────────────┘ │ HTML │ └─────────────┘
└─────────────┘
│
完全なHTMLを
サーバーで生成
メリット: 初期表示が速い、SEOに有利 デメリット: サーバー負荷、TTFBが遅くなる可能性。
SSG(Static Site Generation)
ビルド時にHTMLを生成します。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Build Time │ ───> │ Static HTML │ ───> │ CDN │
│ Generate │ │ Files │ │ Serve │
└─────────────┘ └─────────────┘ └─────────────┘
│
ビルド時に
HTMLを生成
メリット: 最速の配信、高いスケーラビリティ デメリット: 動的コンテンツに不向き。
プロジェクトの作成
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npm run dev
ディレクトリ構成(App Router)
my-app/
├── app/
│ ├── layout.tsx # ルートレイアウト
│ ├── page.tsx # ホームページ(/)
│ ├── globals.css # グローバルCSS
│ ├── about/
│ │ └── page.tsx # Aboutページ(/about)
│ └── blog/
│ ├── page.tsx # ブログ一覧(/blog)
│ └── [slug]/
│ └── page.tsx # 動的ルート(/blog/xxx)
├── components/
├── public/
├── next.config.ts
└── package.json
App Router
Next.js 13 で導入された新しいルーティングシステムで、現在はこれが標準です。Server Components を前提とし、Suspense・Streaming・Server Actions を活用できます。
Next.js には pages/ ディレクトリを使う旧来の Pages Router もありますが、新規プロジェクトでは App Router を採用してください。Pages Router は既存プロジェクトの互換維持のために残されている legacy 扱いで、新機能('use cache'、Partial Prerendering など)は App Router でのみ利用可能です。
Next.js 16(最新安定版)では Turbopack がデフォルトバンドラーとして stable 化されました。開発(next dev)・本番ビルド(next build)のどちらも Turbopack が使われ、Webpack 時代と比べて大幅に高速化しています。特別な設定なく新規プロジェクトから恩恵を受けられます。
基本的なページ
// app/page.tsx - ホームページ
export default function HomePage() {
return (
<main>
<h1>Welcome to Next.js</h1>
</main>
)
}
// app/about/page.tsx - /about
export default function AboutPage() {
return (
<main>
<h1>About Us</h1>
</main>
)
}
レイアウト
複数のページで共有されるUIを定義します。
// app/layout.tsx
import { ReactNode } from 'react'
import './globals.css'
export const metadata = {
title: 'My App',
description: 'My awesome app'
}
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="ja">
<body>
<header className="bg-gray-800 text-white p-4">
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>{children}</main>
<footer className="bg-gray-100 p-4">
© {new Date().getFullYear()} My App
</footer>
</body>
</html>
)
}
動的ルート
URLパラメータを使用するページです。
// app/blog/[slug]/page.tsx
type BlogPostPageProps = {
params: Promise<{ slug: string }>
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const { slug } = await params
// データを取得
const post = await getPost(slug)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
// 静的パスを生成(SSG)
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({
slug: post.slug
}))
}
Server Components と Client Components
Server ComponentsとClient Componentsの使い分けは、Next.js App Routerを使う上で最も重要な概念です。「デフォルトはServer Component、インタラクションが必要な部分だけClient Component」と覚えておきましょう。
Next.js App Routerでは、デフォルトでServer Componentsが使用されます。
Server Components
サーバーでのみ実行されるコンポーネントです。
// app/users/page.tsx(Server Component)
// データベースに直接アクセス可能
import { db } from '@/lib/db'
export default async function UsersPage() {
// サーバーで実行される
const users = await db.user.findMany()
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
Server ComponentsではuseState、useEffectなどのクライアント側フックは使用できません。インタラクティブな機能が必要な場合は、その部分だけを'use client'を付けた別コンポーネントに分離してください。
Server Componentsの特徴:
- データベースに直接アクセス可能
- APIキーなどの機密情報を安全に使用
- バンドルサイズに含まれない
- useState、useEffectは使用不可
Client Components
ブラウザで実行されるコンポーネントです。'use client'ディレクティブを付けます。
// components/Counter.tsx(Client Component)
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(c => c + 1)
}
return (
<button onClick={handleClick}>
Count: {count}
</button>
)
}
Client Componentsを使う場面:
useState、useEffectなどのHooksを使用- ブラウザAPIを使用(localStorage、window等)
- イベントハンドラが必要
- サードパーティライブラリがクライアント専用
使い分け
// app/dashboard/page.tsx(Server Component)
import { db } from '@/lib/db'
import { InteractiveChart } from '@/components/InteractiveChart'
export default async function DashboardPage() {
// サーバーでデータ取得
const data = await db.analytics.getStats()
return (
<div>
<h1>Dashboard</h1>
{/* Server Componentでデータを取得し、Client Componentに渡す */}
<InteractiveChart data={data} />
</div>
)
}
// components/InteractiveChart.tsx(Client Component)
'use client'
import { useState } from 'react'
type ChartProps = {
data: number[]
}
export function InteractiveChart({ data }: ChartProps) {
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
const createSelectHandler = (index: number) => () => {
setSelectedIndex(index)
}
return (
<div>
{data.map((value, index) => (
<div
key={index}
onClick={createSelectHandler(index)}
style={{ opacity: selectedIndex === index ? 1 : 0.7 }}
>
{value}
</div>
))}
</div>
)
}
データ取得
Server Componentsでのデータ取得
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
// キャッシュ設定
next: { revalidate: 60 } // 60秒ごとに再検証
})
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: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
キャッシュオプション
Next.js 15 以降、fetch はデフォルトでキャッシュされなくなりました(no-store 相当)。キャッシュしたい場合は明示的にオプションを指定します。
// 動的データ(デフォルト) - リクエストごとに取得
fetch(url)
fetch(url, { cache: 'no-store' }) // 同じ意味(明示形)
// 永続キャッシュ - ビルド時または初回取得時にキャッシュし、以降は再利用
fetch(url, { cache: 'force-cache' })
// 時間ベースの再検証
fetch(url, { next: { revalidate: 60 } }) // 60秒ごとに再検証
Next.js 14 以前は fetch(url) がデフォルトでキャッシュされていました。Next.js 15 で挙動が反転し、デフォルトは毎回フレッシュなデータを取得するように変更されました。キャッシュが必要なら force-cache を明示してください。
'use cache' ディレクティブ(Cache Components、Next.js 16)
関数や Server Component 単位でキャッシュを宣言できる 'use cache' ディレクティブが追加されました。fetch だけでなく、データベースアクセスや重い計算結果もキャッシュできます。
// 関数単位でキャッシュ
async function getPosts() {
'use cache'
const posts = await db.posts.findMany()
return posts
}
// Server Component単位でキャッシュ
export default async function Page() {
'use cache'
const data = await fetchHeavyData()
return <Article data={data} />
}
'use cache' は Cache Components の機能です。Next.js 16 では next.config.ts で cacheComponents: true を有効にすると使えます(Next.js 15 では experimental 扱いでした)。有効化後は、fetch のキャッシュ制御だけでなく任意の非同期処理をキャッシュできるようになります。
Server Actions
フォーム送信などのサーバー処理を簡潔に書けます。
// app/contact/page.tsx
import { redirect } from 'next/navigation'
async function submitForm(formData: FormData) {
'use server'
const name = formData.get('name') as string
const email = formData.get('email') as string
// データベースに保存
await db.contact.create({
data: { name, email }
})
// リダイレクト
redirect('/contact/thanks')
}
export default function ContactPage() {
return (
<form action={submitForm}>
<input name="name" placeholder="お名前" required />
<input name="email" type="email" placeholder="メール" required />
<button type="submit">送信</button>
</form>
)
}
useActionStateとの組み合わせ
Next.jsの最新版(15以降)はReact 19を使用しています。useActionStateはReact 19で導入されたフックです。Server ComponentsもReact 19で安定版となりました。
// components/ContactForm.tsx
'use client'
import { useActionState } from 'react'
import { submitContact } from '@/app/actions'
export function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, null)
return (
<form action={formAction}>
<input name="name" placeholder="お名前" required />
<input name="email" type="email" placeholder="メール" required />
{state?.error && (
<p className="text-red-500">{state.error}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? '送信中...' : '送信'}
</button>
</form>
)
}
// app/actions.ts
'use server'
import { z } from 'zod'
const schema = z.object({
name: z.string().min(1),
email: z.email()
})
export async function submitContact(prevState: unknown, formData: FormData) {
const validatedFields = schema.safeParse({
name: formData.get('name'),
email: formData.get('email')
})
if (!validatedFields.success) {
return { error: 'Invalid input' }
}
await db.contact.create({
data: validatedFields.data
})
return { success: true }
}
Route Handlers(API の作成)
バックエンドAPIを同一プロジェクトで構築できます。Pages Router の API Routes に相当する機能で、App Router では Route Handlers と呼びます。
// app/api/users/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const users = await db.user.findMany()
return NextResponse.json(users)
}
export async function POST(request: Request) {
const body = await request.json()
const user = await db.user.create({ data: body })
return NextResponse.json(user, { status: 201 })
}
// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server'
type Params = {
params: Promise<{ id: string }>
}
export async function GET(request: Request, { params }: Params) {
const { id } = await params
const user = await db.user.findUnique({ where: { id } })
if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
return NextResponse.json(user)
}
ナビゲーション
Linkコンポーネント
import Link from 'next/link'
export function Navigation() {
return (
<nav>
<Link href="/">ホーム</Link>
<Link href="/about">About</Link>
<Link href="/blog/hello-world">記事</Link>
</nav>
)
}
プログラムによるナビゲーション
'use client'
import { useRouter } from 'next/navigation'
export function LoginButton() {
const router = useRouter()
const handleLogin = async () => {
await login()
router.push('/dashboard')
}
return <button onClick={handleLogin}>ログイン</button>
}
画像の最適化
import Image from 'next/image'
export function Avatar({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={100}
height={100}
className="rounded-full"
/>
)
}
Imageコンポーネントの特徴:
- 自動的に最適なサイズに変換
- WebPなどの最新フォーマットを自動選択
- 遅延読み込み(lazy loading)
- レイアウトシフト防止
メタデータ
// app/layout.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: {
default: 'My App',
template: '%s | My App'
},
description: 'My awesome application',
openGraph: {
title: 'My App',
description: 'My awesome application',
images: ['/og-image.png']
}
}
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
type Props = {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.excerpt
}
}
Vite + Reactとの比較
| 項目 | Vite + React | Next.js |
|---|---|---|
| レンダリング | CSRのみ | SSR/SSG/CSR |
| ルーティング | 手動(React Router等) | ファイルベース |
| データ取得 | クライアントのみ | サーバー/クライアント |
| SEO | 追加設定が必要 | 標準サポート |
| デプロイ | 静的ホスティング | Vercel/Node.jsサーバー |
| 学習コスト | 低い | 中程度 |
| 適用場面 | SPA、ダッシュボード | Webサイト、フルスタック |
いつNext.jsを使うべきか
Next.jsが適している場合
- SEOが重要なWebサイト
- コンテンツ中心のサイト(ブログ、ECなど)
- フルスタック開発(API含む)
- 初期表示速度が重要
Vite + Reactが適している場合
- 管理画面、ダッシュボード
- 認証後のアプリ(SEO不要)
- シンプルなSPA
- 学習目的
試してみよう
以下の課題に挑戦して、Next.jsの理解を深めましょう。
-
Next.jsプロジェクトを作成する:
npx create-next-app@latestでプロジェクトを作成し、/aboutと/contactページを追加してください。共通のヘッダーとフッターをlayout.tsxに配置しましょう。 -
Server ComponentとClient Componentを使い分ける: ダミーのユーザーリストをServer Componentで表示し、「いいね」ボタンの機能をClient Componentで実装してください。
'use client'の配置場所を意識しましょう。 -
Server Actionでフォームを作る: お問い合わせフォーム(名前、メール、メッセージ)を作成し、Server Actionで送信処理を実装してください。送信後に「送信完了」メッセージを表示しましょう。
ヒント
app/about/page.tsxとapp/contact/page.tsxを作成します。layout.tsxの{children}の前後にヘッダー/フッターを配置します。- ユーザーリストは
page.tsx(Server Component)で、いいねボタンは別ファイルに'use client'を付けて作成します。 'use server'を関数の先頭に付けるか、別ファイル(actions.ts)にServer Actionを定義します。useActionStateを使うとローディング状態も管理できます。
回答と解説
課題1: Next.jsプロジェクトを作成する
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
// app/layout.tsx
import { ReactNode } from 'react'
import Link from 'next/link'
import './globals.css'
export const metadata = {
title: 'My App',
description: 'Next.js練習アプリ'
}
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="ja">
<body>
<header className="bg-blue-600 text-white p-4">
<nav className="max-w-4xl mx-auto flex gap-6">
<Link href="/" className="hover:underline">ホーム</Link>
<Link href="/about" className="hover:underline">About</Link>
<Link href="/contact" className="hover:underline">お問い合わせ</Link>
</nav>
</header>
<main className="max-w-4xl mx-auto p-4">{children}</main>
<footer className="bg-gray-100 p-4 text-center text-gray-600">
© {new Date().getFullYear()} My App
</footer>
</body>
</html>
)
}
// app/about/page.tsx
export default function AboutPage() {
return (
<div>
<h1 className="text-2xl font-bold mb-4">About</h1>
<p>このアプリはNext.jsの練習用に作成しました。</p>
</div>
)
}
// app/contact/page.tsx
export default function ContactPage() {
return (
<div>
<h1 className="text-2xl font-bold mb-4">お問い合わせ</h1>
<p>お問い合わせはこちらからどうぞ。</p>
</div>
)
}
layout.tsxに定義したヘッダーとフッターは、すべてのページで共有されます。Linkコンポーネントを使うと、クライアントサイドナビゲーションが行われ、ページ遷移が高速になります。
課題2: Server ComponentとClient Componentを使い分ける
// app/users/page.tsx(Server Component)
import { LikeButton } from './LikeButton'
// ダミーユーザーデータ(実際はDBから取得)
const users = [
{ id: '1', name: '田中太郎', email: 'tanaka@example.com' },
{ id: '2', name: '鈴木花子', email: 'suzuki@example.com' },
{ id: '3', name: '佐藤次郎', email: 'sato@example.com' }
]
export default function UsersPage() {
return (
<div>
<h1 className="text-2xl font-bold mb-4">ユーザー一覧</h1>
<ul className="space-y-4">
{users.map((user) => (
<li key={user.id} className="p-4 border rounded flex justify-between items-center">
<div>
<p className="font-semibold">{user.name}</p>
<p className="text-gray-600 text-sm">{user.email}</p>
</div>
<LikeButton userId={user.id} />
</li>
))}
</ul>
</div>
)
}
// app/users/LikeButton.tsx(Client Component)
'use client'
import { useState } from 'react'
type LikeButtonProps = {
userId: string
}
export function LikeButton({ userId }: LikeButtonProps) {
const [liked, setLiked] = useState(false)
const [count, setCount] = useState(0)
const handleClick = () => {
setLiked(!liked)
setCount(liked ? count - 1 : count + 1)
}
return (
<button
onClick={handleClick}
className={`px-4 py-2 rounded ${
liked ? 'bg-red-500 text-white' : 'bg-gray-200 text-gray-700'
}`}
>
{liked ? '❤️' : '🤍'} {count}
</button>
)
}
Server Component(page.tsx)でデータを取得・表示し、インタラクティブな「いいね」機能はClient Component(LikeButton.tsx)に分離しています。'use client'はファイルの先頭に配置する必要があります。
課題3: Server Actionでフォームを作る
// app/contact/actions.ts
'use server'
type ContactState = {
success?: boolean
error?: string
}
export async function submitContact(
prevState: ContactState | null,
formData: FormData
): Promise<ContactState> {
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
// バリデーション
if (!name || !email || !message) {
return { error: 'すべての項目を入力してください' }
}
// 実際はここでDBに保存やメール送信を行う
console.log('お問い合わせ受信:', { name, email, message })
// 擬似的な遅延
await new Promise((resolve) => setTimeout(resolve, 1000))
return { success: true }
}
// app/contact/ContactForm.tsx
'use client'
import { useActionState } from 'react'
import { submitContact } from './actions'
export function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, null)
if (state?.success) {
return (
<div className="p-4 bg-green-100 text-green-700 rounded">
送信完了しました。お問い合わせありがとうございます。
</div>
)
}
return (
<form action={formAction} className="space-y-4">
{state?.error && (
<div className="p-4 bg-red-100 text-red-700 rounded">{state.error}</div>
)}
<div>
<label htmlFor="name" className="block font-medium mb-1">お名前</label>
<input
id="name"
name="name"
type="text"
required
className="w-full px-3 py-2 border rounded"
/>
</div>
<div>
<label htmlFor="email" className="block font-medium mb-1">メールアドレス</label>
<input
id="email"
name="email"
type="email"
required
className="w-full px-3 py-2 border rounded"
/>
</div>
<div>
<label htmlFor="message" className="block font-medium mb-1">メッセージ</label>
<textarea
id="message"
name="message"
rows={4}
required
className="w-full px-3 py-2 border rounded"
/>
</div>
<button
type="submit"
disabled={isPending}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{isPending ? '送信中...' : '送信'}
</button>
</form>
)
}
// app/contact/page.tsx
import { ContactForm } from './ContactForm'
export default function ContactPage() {
return (
<div>
<h1 className="text-2xl font-bold mb-4">お問い合わせ</h1>
<ContactForm />
</div>
)
}
Server Action(actions.ts)はサーバー側で実行されるため、データベース操作や機密情報を安全に扱えます。useActionStateを使うことで、送信中状態(isPending)とサーバーからの応答(state)を簡単に管理できます。
まとめ
- Next.jsはReactのフルスタックフレームワーク
- SSR/SSG/CSRを柔軟に選択可能
- App Routerでファイルベースルーティング
- Server ComponentsとClient Componentsの使い分け
- Server Actionsでフォーム処理を簡潔に
- 画像やフォントの自動最適化
本書で学んだReactの知識は、Next.jsでもそのまま活用できます。Reactの基礎を固めた上で、プロジェクトの要件に応じてNext.jsの採用を検討してください。
次の章では、学んだ知識を総動員した実践プロジェクトに取り組みます。
次に読む
総合演習 へ進みます。総合演習として実践プロジェクトを 1 つ完成させます。