プロジェクト構造
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
ページとレイアウト
ページ
export default function Home() {
return <h1>ホーム</h1>;
}
interface PageProps {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default function BlogPost({ params, searchParams }: PageProps) {
return <h1>{params.slug}</h1>;
}
レイアウト
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}
データフェッチ
Server Components
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 },
});
並列フェッチ
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
"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
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 });
}
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);
}
ナビゲーション
Link
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();
const searchParams = useSearchParams();
const page = searchParams.get("page");
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
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*"],
};
環境変数
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
const dbUrl = process.env.DATABASE_URL;
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
next.config.js
const nextConfig = {
images: {
domains: ["example.com"],
},
async redirects() {
return [
{ source: "/old", destination: "/new", permanent: true },
];
},
env: {
customKey: "value",
},
};
module.exports = nextConfig;
関連記事
← 一覧に戻る