はじめに
フロントエンドはずっとjQueryかVue.jsで書いてきた。
Reactは「なんとなく難しそう」という印象で避けてきたが、Next.jsをやる機会ができたので腰を上げた。PHPエンジニアとして「サーバーサイドでHTMLを生成する」という発想は持っていたので、SSRの概念は意外とすんなり入ってきた。逆にReactのコンポーネント思考やJSXは最初かなり戸惑った。
インストールとプロジェクト作成
npx create-next-app@latest my-app
対話形式の設定:
✓ TypeScript を使いますか? → Yes
✓ ESLintを使いますか? → Yes
✓ Tailwind CSSを使いますか?→ Yes
✓ src/ディレクトリを使いますか?→ No
✓ App Routerを使いますか? → Yes(最新の推奨)
✓ デフォルトのインポートエイリアスを変更しますか?→ No
cd my-app
npm run dev
# http://localhost:3000 で起動
生成されるディレクトリ構造:
my-app/
├── app/ # App Router(ルーティングの中心)
│ ├── layout.tsx # 共通レイアウト
│ ├── page.tsx # / のページ
│ └── globals.css
├── public/ # 静的ファイル
├── components/ # 再利用可能なコンポーネント
├── next.config.mjs # Next.jsの設定
├── tailwind.config.ts
├── tsconfig.json
└── package.json
PHPとの発想の違い
まず概念を整理した。
PHP(Laravel):
ブラウザ → サーバー → PHPがHTMLを生成 → ブラウザに返す
ページごとにHTMLを丸ごと返す
Next.js(SPA + SSR):
初回: サーバーがHTMLを生成 → ブラウザに返す(SSR)
以降: JavaScriptがDOM操作 → ページ遷移がサーバーリクエストなし(SPA)
必要なデータだけAPIから取得
SSR(Server Side Rendering)はPHPに似た発想。「サーバーでHTMLを作ってブラウザに返す」という点は同じ。違うのは初回ロード後の動作で、Next.jsはその後JavaScriptが引き継いでSPAとして動く。
JSXとコンポーネントの基本
最初に戸惑ったのがJSX。HTMLとJavaScriptが混在している書き方。
// PHPのテンプレート
// <p>こんにちは、<?= $name ?>さん</p>
// JSX(Next.js / React)
function Greeting({ name }: { name: string }) {
return <p>こんにちは、{name}さん</p>;
}
JavaScriptの中にHTMLを書く感覚。PHPのEchoとは逆の発想。最初は気持ち悪かったが慣れると読みやすい。
// components/UserCard.tsx
type User = {
id: number;
name: string;
email: string;
};
export default function UserCard({ user }: { user: User }) {
return (
<div className="p-4 border rounded-lg">
<h2 className="text-lg font-bold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
</div>
);
}
コンポーネントは関数。引数(props)でデータを受け取って、JSXを返す。PHPのincludeでパーツを再利用していたのがコンポーネントに置き換わる。
App Router — ファイルベースルーティング
Next.jsのApp Routerはapp/ディレクトリの構造がそのままURLになる。
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── users/
│ ├── page.tsx → /users
│ └── [id]/
│ └── page.tsx → /users/123(動的ルート)
└── blog/
└── [slug]/
└── page.tsx → /blog/my-first-post
LaravelのルートファイルにURLを定義する代わりに、ファイルを置くだけでルートが作られる。
// app/users/[id]/page.tsx
type Props = {
params: { id: string };
};
export default function UserPage({ params }: Props) {
return (
<div>
<h1>ユーザーID: {params.id}</h1>
</div>
);
}
[id]がURLパラメータに対応する。LaravelのRoute::get('/users/{id}')に相当する部分がファイル名になっている。
Server ComponentとClient Component
App Routerで最も重要な概念。最初はここで一番混乱した。
// Server Component(デフォルト)
// サーバーで実行される。DBアクセスやAPIコールが直接書ける
async function UserList() {
// サーバーで直接DBを叩ける
const users = await fetch("https://api.example.com/users").then(r => r.json());
return (
<ul>
{users.map((user: User) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Client Component('use client'を先頭に書く)
// ブラウザで実行される。useState/useEffectが使える
'use client';
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>増やす</button>
</div>
);
}
Server Component:
✓ データフェッチ(DBアクセス、APIコール)
✓ 機密情報(APIキーなど)を含む処理
✓ 大きいライブラリの利用(バンドルサイズに影響しない)
✗ useState, useEffect は使えない
✗ onClick などのイベントハンドラは使えない
Client Component:
✓ インタラクティブな操作(ボタン、フォーム)
✓ useState, useEffect
✓ ブラウザのAPIを使う処理
✗ サーバーのリソースに直接アクセスできない
PHPにはこの区別がない。PHPは全部サーバーで動くし、インタラクティブな部分はJavaScriptを別途書く。Next.jsはコンポーネントレベルでどこで動かすかを制御できる。
データフェッチの基本
Server Componentでのデータ取得
// app/users/page.tsx
type User = {
id: number;
name: string;
email: string;
};
// async関数でServer Componentを書く
async function UsersPage() {
// サーバーで実行されるのでAPIキーを隠せる
const response = await fetch('https://api.example.com/users', {
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`,
},
next: { revalidate: 60 }, // 60秒ごとに再フェッチ
});
const users: User[] = await response.json();
return (
<main>
<h1>ユーザー一覧</h1>
<ul>
{users.map(user => (
<li key={user.id}>
<a href={`/users/${user.id}`}>{user.name}</a>
</li>
))}
</ul>
</main>
);
}
export default UsersPage;
PHP的な書き方に近い。サーバーでデータを取得してHTMLとして返す。
Client Componentでのデータ取得
// ブラウザ上でのデータ取得
'use client';
import { useState, useEffect } from 'react';
type User = { id: number; name: string };
function UserSearch() {
const [query, setQuery] = useState('');
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) return;
setLoading(true);
fetch(`/api/users?q=${query}`)
.then(r => r.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, [query]); // queryが変わるたびに実行
return (
<div>
<input
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="ユーザーを検索"
/>
{loading && <p>検索中...</p>}
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
useStateで状態を管理、useEffectで副作用(データ取得)を書く。最初は「なんで普通に変数を使ったらダメなの?」と思ったが、「状態が変わったらUIを再レンダリングする」仕組みを使うために必要だと理解した。
Route Handler — APIエンドポイントを作る
Next.js内でAPIを作れる。FastAPIとの連携前に、Next.js内部でAPIを作る場合はこれを使う。
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/users
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('q');
const users = [
{ id: 1, name: '田中', email: 'tanaka@example.com' },
{ id: 2, name: '鈴木', email: 'suzuki@example.com' },
].filter(u => !query || u.name.includes(query));
return NextResponse.json(users);
}
// POST /api/users
export async function POST(request: NextRequest) {
const body = await request.json();
const { name, email } = body;
if (!name || !email) {
return NextResponse.json(
{ error: 'nameとemailは必須です' },
{ status: 422 }
);
}
// DBに保存する処理(省略)
return NextResponse.json({ id: 99, name, email }, { status: 201 });
}
Laravelのコントローラーに近い書き方。ファイル名がroute.tsで、エクスポートする関数名がHTTPメソッドになる。
レイアウト
// app/layout.tsx(全ページ共通のレイアウト)
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'My App',
description: 'Next.jsのサンプルアプリ',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>
<header className="bg-blue-600 text-white p-4">
<nav>
<a href="/" className="mr-4">ホーム</a>
<a href="/users">ユーザー</a>
</nav>
</header>
<main className="container mx-auto p-4">
{children}
</main>
<footer className="bg-gray-100 p-4 text-center">
Footer
</footer>
</body>
</html>
);
}
LaravelのBladeレイアウトに相当する。{children}で各ページのコンテンツが入る。
ネストしたレイアウト
// app/users/layout.tsx(/users以下だけに適用されるレイアウト)
export default function UsersLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<aside className="sidebar">
<nav>
<a href="/users">一覧</a>
<a href="/users/new">新規作成</a>
</nav>
</aside>
<section className="content">
{children}
</section>
</div>
);
}
app/users/layout.tsxは/users以下のページだけに適用される。Laravelのネストしたビューより直感的に理解できた。
環境変数
# .env.local
DATABASE_URL=postgresql://...
API_KEY=secret-key-here # サーバーのみ
NEXT_PUBLIC_API_URL=https://api.example.com # ブラウザでも使える
NEXT_PUBLIC_プレフィックスをつけるとブラウザのJavaScriptからも参照できる。つけないとサーバーサイドのみ。PHPの$_ENVとは違い、ブラウザに露出するかどうかを命名で制御する。
// サーバーでのみ使える(NEXT_PUBLIC_なし)
const apiKey = process.env.API_KEY; // サーバーコンポーネントでのみ
// ブラウザでも使える(NEXT_PUBLIC_あり)
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // どこでも使える
リンクとナビゲーション
import Link from 'next/link';
import { useRouter } from 'next/navigation';
// リンク(ページ遷移のデフォルト手段)
function Navigation() {
return (
<nav>
<Link href="/">ホーム</Link>
<Link href="/users">ユーザー一覧</Link>
<Link href="/users/1">田中さん</Link>
</nav>
);
}
// プログラムからのナビゲーション(Client Componentのみ)
'use client';
function LoginForm() {
const router = useRouter();
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// ログイン処理...
router.push('/dashboard'); // ログイン後にリダイレクト
}
return <form onSubmit={handleSubmit}>...</form>;
}
<a>タグの代わりに<Link>を使う。Next.jsが差分更新(SPA的な遷移)をしてくれる。
PHPとNext.jsの対応表
| PHP(Laravel) | Next.js |
|---|---|
routes/web.php |
app/のディレクトリ構造 |
| Bladeテンプレート | JSX / コンポーネント |
@extends / @section
|
layout.tsx / {children}
|
@include |
コンポーネントのimport |
{{ $variable }} |
{variable} |
@foreach |
.map() |
@if |
{condition && <JSX>} |
| コントローラー |
page.tsx / route.ts
|
| ミドルウェア | middleware.ts |
.env |
.env.local |
まとめ
- App Routerはファイル構造がURLになる(LaravelのRoute定義が不要)
- Server ComponentはデフォルトでサーバーサイドでHTMLを生成(PHPに近い)
- Client Componentは
'use client'を先頭に書く。インタラクティブな処理に使う -
useStateで状態管理、useEffectで副作用の処理 -
layout.tsxがBladeのテンプレート継承に相当する - 環境変数は
NEXT_PUBLIC_の有無でブラウザへの露出を制御する
PHPエンジニアとしてSSRの概念はすんなり入ったが、コンポーネント思考とServer/Clientの使い分けが最初の壁だった。「サーバーで動くかブラウザで動くか」を常に意識する習慣がつくと一気に書きやすくなった。