Next.jsチートシート

中級 | 15分 で読める | 2025.01.10

プロジェクト構造

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: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
}

export default function BlogPost({ params, searchParams }: PageProps) {
  return <h1>{params.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>
  );
}

データフェッチ

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>
  );
}

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>
  );
}

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 post = await getPost(params.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: { slug: string } }) {
  const post = await getPost(params.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*"],
};

環境変数

# .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;

関連記事

← 一覧に戻る