4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js入門 — PHPエンジニアがReactを始めてみた

4
Posted at

はじめに

フロントエンドはずっと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の使い分けが最初の壁だった。「サーバーで動くかブラウザで動くか」を常に意識する習慣がつくと一気に書きやすくなった。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?