Next.jsチートシート

中級 | 15分 で読める | 2025.01.10

公式ドキュメント

概要

Next.js は React をベースにしたフルスタック Web フレームワークです。このチートシートでは、App Router を前提に、ページ、レイアウト、Server Components、データフェッチ、Server Actions、Route Handlers、メタデータの基本形を整理します。

最初に覚える最小セット

ファイル / API用途
app/layout.tsx共通レイアウト
app/page.tsxルートページ
app/blog/[slug]/page.tsx動的ルート
loading.tsx / error.tsx / not-found.tsx状態別 UI
fetch(..., { next: { revalidate } })再検証つきデータ取得
"use client"Client Component の明示
"use server"Server Action の明示

App Router では、サーバーでできる処理は Server Component に残し、ブラウザ操作が必要な部分だけ Client Component にします。秘密情報はクライアントへ渡さず、環境変数の公開範囲を必ず確認します。

プロジェクト構造

App Router の標準構造(公式: https://nextjs.org/docs/app/getting-started/project-structure):

app/
├── layout.tsx # ルートレイアウト
├── page.tsx # ホームページ (/)
├── loading.tsx # ローディングUI
├── error.tsx # エラーUI
├── not-found.tsx # 404ページ
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
├── api/
│ └── users/
│ └── route.ts # /api/users
└── (marketing)/ # ルートグループ
 └── about/
 └── page.tsx # /about

ページとレイアウト

ページ

// app/page.tsx
export default function Home() {
 return <h1>ホーム</h1>;
}

// app/blog/[slug]/page.tsx
interface PageProps {
 params: Promise<{ slug: string }>;
 searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function BlogPost({ params, searchParams }: PageProps) {
 const { slug } = await params;
 return <h1>{slug}</h1>;
}

レイアウト

// app/layout.tsx
export default function RootLayout({
 children,
}: {
 children: React.ReactNode;
}) {
 return (
 <html lang="ja">
 <body>
 <Header />
 {children}
 <Footer />
 </body>
 </html>
 );
}

// ネストしたレイアウト
// app/dashboard/layout.tsx
export default function DashboardLayout({
 children,
}: {
 children: React.ReactNode;
}) {
 return (
 <div className="dashboard">
 <Sidebar />
 <main>{children}</main>
 </div>
 );
}

ポイント: layout.tsxは子ページ間で共有されるUI。ヘッダー・サイドバーなど共通パーツはlayoutに配置し、ページ固有のコンテンツはpage.tsxに書きます。

データフェッチ

Server Components

// 直接async/await
async function getData() {
 const res = await fetch("https://api.example.com/data");
 return res.json();
}

export default async function Page() {
 const data = await getData();
 return <div>{data.title}</div>;
}

// キャッシュ制御
const data = await fetch("https://api.example.com/data", {
 cache: "no-store", // キャッシュなし
});

const data = await fetch("https://api.example.com/data", {
 next: { revalidate: 60 }, // 60秒ごとに再検証
});

並列フェッチ

export default async function Page() {
 const [user, posts] = await Promise.all([
 getUser(),
 getPosts(),
 ]);

 return (
 <>
 <UserProfile user={user} />
 <PostList posts={posts} />
 </>
 );
}

Suspense

import { Suspense } from "react";

export default function Page() {
 return (
 <div>
 <h1>ダッシュボード</h1>
 <Suspense fallback={<Loading />}>
 <SlowComponent />
 </Suspense>
 </div>
 );
}

実践メモ: 複数のAPIを呼ぶ場合はPromise.allで並列フェッチ。直列に書くとウォーターフォールが発生し、表示が遅くなります。

注意: cache: "no-store"next: { revalidate: N }を同時に指定しないように。キャッシュ戦略はページの特性に応じて1つ選びましょう。

Server Actions

// app/actions.ts
"use server";

import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
 const title = formData.get("title") as string;
 const content = formData.get("content") as string;

 await db.post.create({ data: { title, content } });

 revalidatePath("/posts");
}

// コンポーネントで使用
export default function NewPostForm() {
 return (
 <form action={createPost}>
 <input name="title" required />
 <textarea name="content" required />
 <button type="submit">投稿</button>
 </form>
 );
}

useActionState

"use client";

import { useActionState } from "react";
import { createPost } from "./actions";

export default function Form() {
 const [state, formAction, isPending] = useActionState(createPost, null);

 return (
 <form action={formAction}>
 <input name="title" disabled={isPending} />
 <button disabled={isPending}>
 {isPending ? "送信中..." : "投稿"}
 </button>
 {state?.error && <p>{state.error}</p>}
 </form>
 );
}

ポイント: "use server"を付けた関数はサーバーでのみ実行されます。revalidatePathでキャッシュを無効化し、最新データを表示しましょう。

API Routes

// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
 const searchParams = request.nextUrl.searchParams;
 const page = searchParams.get("page");

 const users = await db.user.findMany();
 return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
 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
export async function GET(
 request: NextRequest,
 { params }: { params: { id: string } }
) {
 const user = await db.user.findUnique({ where: { id: params.id } });
 if (!user) {
 return NextResponse.json({ error: "Not found" }, { status: 404 });
 }
 return NextResponse.json(user);
}

ナビゲーション

import Link from "next/link";

<Link href="/about">About</Link>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
<Link href="/dashboard" prefetch={false}>Dashboard</Link>

useRouter

"use client";

import { useRouter } from "next/navigation";

export default function Component() {
 const router = useRouter();

 return (
 <button onClick={() => router.push("/dashboard")}>
 ダッシュボードへ
 </button>
 );
}

// メソッド
router.push("/path"); // 遷移
router.replace("/path"); // 履歴を置換
router.back(); // 戻る
router.forward(); // 進む
router.refresh(); // 再フェッチ

usePathname / useSearchParams

"use client";

import { usePathname, useSearchParams } from "next/navigation";

export default function Component() {
 const pathname = usePathname(); // "/blog/hello"
 const searchParams = useSearchParams();
 const page = searchParams.get("page"); // "1"

 return <div>{pathname}</div>;
}

メタデータ

// 静的メタデータ
export const metadata = {
 title: "ページタイトル",
 description: "ページの説明",
};

// 動的メタデータ
export async function generateMetadata({ params }: PageProps) {
 const { slug } = await params;
 const post = await getPost(slug);
 return {
 title: post.title,
 description: post.excerpt,
 openGraph: {
 title: post.title,
 images: [post.image],
 },
 };
}

静的生成

// 静的パスの生成
export async function generateStaticParams() {
 const posts = await getPosts();
 return posts.map((post) => ({
 slug: post.slug,
 }));
}

export default async function Page({
 params,
}: {
 params: Promise<{ slug: string }>;
}) {
 const { slug } = await params;
 const post = await getPost(slug);
 return <article>{post.content}</article>;
}

Middleware

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
 // 認証チェック
 const token = request.cookies.get("token");
 if (!token) {
 return NextResponse.redirect(new URL("/login", request.url));
 }

 return NextResponse.next();
}

export const config = {
 matcher: ["/dashboard/:path*", "/api/:path*"],
};

注意: NEXT_PUBLIC_プレフィックスのない環境変数はクライアントに公開されません。ブラウザ側で使う値には必ずNEXT_PUBLIC_を付けましょう。逆に、秘密情報にはこのプレフィックスを付けないように。

実践メモ: useRouterはClient Componentでのみ使用可能。Server Componentからのリダイレクトはredirect()関数を使います。

環境変数

# .env.local
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
// サーバーサイドのみ
const dbUrl = process.env.DATABASE_URL;

// クライアントでも使用可能(NEXT_PUBLIC_プレフィックス)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
 // 画像ドメイン許可
 images: {
 domains: ["example.com"],
 },
 // リダイレクト
 async redirects() {
 return [
 { source: "/old", destination: "/new", permanent: true },
 ];
 },
 // 環境変数
 env: {
 customKey: "value",
 },
};

module.exports = nextConfig;

まとめ

Next.js App Router では、ページ構造、Server Component / Client Component の境界、fetch のキャッシュ戦略、環境変数の公開範囲を押さえることが重要です。"use client" を広げすぎるとバンドルが重くなり、秘密情報の扱いも難しくなります。

paramssearchParams、Server Actions、キャッシュ API はバージョン差が出やすい領域です。実装前に公式ドキュメントで現在の型と推奨パターンを確認してください。

参考リソース

注: paramssearchParams の型は Next.js のバージョンで変わります。最新版の API は公式ドキュメントを確認してください。

関連記事

← 一覧に戻る
PR
PR
PR
PR