nextjs-development について
App Router とフルスタック構成で Next.js アプリを開発するためのスキルです。ルーティング、レンダリング、データ取得、API ハンドラー、本番向けの実践を扱い、Web アプリやコンテンツサイトに役立ちます。
主な機能
- 01App Router とファイルベースルーティング
- 02Server Components と Client Components
- 03キャッシュとデータ再検証
- 04API Route Handlers とミドルウェア
- 05メタデータと SEO のパターン
- 06画像最適化と Server Actions
活用シーン
- 01フルスタック React アプリの構築
- 02段階的な再検証を行う静的サイトの作成
- 03API バックエンドとルートハンドラーの実装
- 04SEO を重視したコンテンツサイトの開発
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ manutej/ luxor-claude-marketplace --skill nextjs-developmentインストール後、Claude Code で/nextjs-developmentを入力して読み込まれたか確認します。
同梱ファイルEXAMPLES.mdREADME.md
詳細
Next.js 開発スキル
このスキルでは、公式の Next.js ドキュメントに基づき、App Router、Server Components、データ取得パターン、ルーティング、API ルート、ミドルウェア、フルスタック開発の手法を活用して、最新の Next.js アプリケーションを構築するための包括的なガイダンスを提供します。
このスキルを使用する場面
次のような場合に、このスキルを使用してください。
- サーバーサイドレンダリング(SSR)を備えたフルスタック React アプリケーションを構築する
- 増分静的再生成(ISR)を使用した静的サイトを作成する
- React Server Components を使用した最新の Web アプリケーションを開発する
- サーバーレスのルートハンドラーで API バックエンドを構築する
- メタデータと Open Graph を使用して、SEO に最適化されたアプリケーションを実装する
- 組み込みの最適化機能を備えた、本番環境対応の Web アプリケーションを作成する
- E コマース、ブログ、ダッシュボード、コンテンツ中心のサイトを構築する
- 認証、データ取得、複雑なルーティングパターンを実装する
- 画像、フォント、パフォーマンスを自動的に最適化する
- エッジコンピューティング機能を備えたサーバーレスアプリケーションをデプロイする
基本概念
App Router
App Router は、React Server Components を基盤とする Next.js の最新のルーティングシステムです。app ディレクトリを使用したファイルベースのルーティングに、拡張された機能を加えます。
基本的なアプリ構成:
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
├── about/
│ └── page.tsx # /about route
└── blog/
├── page.tsx # /blog route
├── [slug]/
│ └── page.tsx # /blog/[slug] dynamic route
└── layout.tsx # Blog layout
ルートレイアウト(必須):
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
ページコンポーネント:
// app/page.tsx
export default function HomePage() {
return (
<main>
<h1>Welcome to Next.js</h1>
<p>Building modern web applications</p>
</main>
)
}
Server Components
Server Components は、サーバー上でレンダリングされる React コンポーネントです。App Router ではデフォルトで使用され、パフォーマンスを向上させます。
Server Component(デフォルト):
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // Static generation
})
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
<h1>Blog Posts</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
Client Component(必要な場合):
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}
Server Components と Client Components の組み合わせ:
// app/dashboard/page.tsx (Server Component)
import ClientCounter from './ClientCounter'
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function DashboardPage() {
const data = await getData()
return (
<div>
<h1>Dashboard</h1>
<p>Server data: {data.value}</p>
{/* Client component for interactivity */}
<ClientCounter />
</div>
)
}
データ取得
Next.js は、ネイティブの fetch() API を自動キャッシュと再検証に対応するよう拡張しています。
静的データ取得(デフォルト):
async function getStaticData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache' // Default, equivalent to getStaticProps
})
return res.json()
}
export default async function Page() {
const data = await getStaticData()
return <div>{data.title}</div>
}
動的データ取得:
async function getDynamicData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store' // Equivalent to getServerSideProps
})
return res.json()
}
export default async function Page() {
const data = await getDynamicData()
return <div>{data.title}</div>
}
再検証(ISR):
async function getRevalidatedData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 } // Revalidate every 60 seconds
})
return res.json()
}
export default async function Page() {
const data = await getRevalidatedData()
return <div>{data.title}</div>
}
並列データ取得:
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`)
return res.json()
}
async function getUserPosts(id: string) {
const res = await fetch(`https://api.example.com/users/${id}/posts`)
return res.json()
}
export default async function UserPage({ params }: { params: { id: string } }) {
// Fetch in parallel
const [user, posts] = await Promise.all([
getUser(params.id),
getUserPosts(params.id)
])
return (
<div>
<h1>{user.name}</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
逐次データ取得:
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`)
return res.json()
}
async function getRecommendations(preferences: string[]) {
const res = await fetch('https://api.example.com/recommendations', {
method: 'POST',
body: JSON.stringify({ preferences })
})
return res.json()
}
export default async function UserPage({ params }: { params: { id: string } }) {
// First fetch user
const user = await getUser(params.id)
// Then fetch recommendations based on user data
const recommendations = await getRecommendations(user.preferences)
return (
<div>
<h1>{user.name}</h1>
<h2>Recommendations</h2>
<ul>
{recommendations.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
)
}
ルーティング
Next.js は app ディレクトリで、ファイルシステムベースのルーティングを使用します。
動的ルート:
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Post: {params.slug}</h1>
}
// Generates static pages for these slugs at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post) => ({
slug: post.slug,
}))
}
キャッチオールルート:
// app/docs/[...slug]/page.tsx
export default function DocsPage({ params }: { params: { slug: string[] } }) {
// /docs/a/b/c -> params.slug = ['a', 'b', 'c']
return <h1>Docs: {params.slug.join('/')}</h1>
}
オプショナルキャッチオールルート:
// app/shop/[[...slug]]/page.tsx
export default function ShopPage({ params }: { params: { slug?: string[] } }) {
// /shop -> params.slug = undefined
// /shop/clothes -> params.slug = ['clothes']
// /shop/clothes/tops -> params.slug = ['clothes', 'tops']
return <h1>Shop: {params.slug?.join('/') || 'All'}</h1>
}
ルートグループ:
app/
├── (marketing)/ # Route group (not in URL)
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
└── (shop)/
├── products/
│ └── page.tsx # /products
└── cart/
└── page.tsx # /cart
並列ルート:
app/
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
└── layout.tsx
// app/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{analytics}
{team}
</>
)
}
インターセプトルート:
app/
├── feed/
│ └── page.tsx
├── photo/
│ └── [id]/
│ └── page.tsx
└── @modal/
└── (.)photo/
└── [id]/
└── page.tsx # Intercepts /photo/[id] when navigating from /feed
レイアウト
レイアウトはページを囲み、ナビゲーションをまたいで状態を保持します。
ネストされたレイアウト:
// app/layout.tsx (Root Layout)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<header>
<nav>Global Navigation</nav>
</header>
{children}
<footer>Global Footer</footer>
</body>
</html>
)
}
// app/dashboard/layout.tsx (Dashboard Layout)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard">
<aside>Dashboard Sidebar</aside>
<main>{children}</main>
</div>
)
}
// app/dashboard/page.tsx
export default function DashboardPage() {
return <h1>Dashboard</h1>
}
テンプレート(ナビゲーション時に再レンダリング):
// app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
return (
<div>
{/* This creates a new instance on each navigation */}
{children}
</div>
)
}
読み込み UI
専用の loading.tsx ファイルを使用すると、React Suspense による読み込み状態を作成できます。
読み込み状態:
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="spinner">
<p>Loading dashboard...</p>
</div>
)
}
// app/dashboard/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function DashboardPage() {
const data = await getData()
return <div>{data.content}</div>
}
Suspense によるストリーミング:
// app/page.tsx
import { Suspense } from 'react'
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 3000))
return <div>Slow data loaded</div>
}
export default function Page() {
return (
<div>
<h1>Page</h1>
<Suspense fallback={<div>Loading slow component...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}
エラー処理
専用の error.tsx ファイルを使用すると、エラーバウンダリでエラーを処理できます。
エラーバウンダリ:
// app/error.tsx
'use client' // Error components must be Client Components
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
グローバルエラー:
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Application Error</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
見つからない場合:
// app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div>
<h2>Page Not Found</h2>
<p>Could not find the requested resource</p>
<Link href="/">Return Home</Link>
</div>
)
}
API ルート
ルートハンドラーを使うと、Web Request API と Response API を使用して API エンドポイントを作成できます。
基本的な API ルート:
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: 'Hello from Next.js!' })
}
動的ルートハンドラー:
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const id = params.id
const post = await db.post.findUnique({ where: { id } })
if (!post) {
return new Response('Post not found', { status: 404 })
}
return Response.json(post)
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.post.delete({ where: { id: params.id } })
return new Response(null, { status: 204 })
}
ボディ付き POST リクエスト:
// app/api/posts/route.ts
export async function POST(request: Request) {
const body = await request.json()
const post = await db.post.create({
data: {
title: body.title,
content: body.content,
}
})
return Response.json(post, { status: 201 })
}
ヘッダー付きリクエスト:
// app/api/protected/route.ts
export async function GET(request: Request) {
const token = request.headers.get('authorization')
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
const user = await verifyToken(token)
return Response.json({ user })
}
検索パラメーター:
// app/api/search/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const query = searchParams.get('q')
const page = searchParams.get('page') || '1'
const results = await search(query, parseInt(page))
return Response.json(results)
}
CORS ヘッダー:
// app/api/public/route.ts
export async function GET(request: Request) {
const data = { message: 'Public API' }
return Response.json(data, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
export async function OPTIONS(request: Request) {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
ミドルウェア
ミドルウェアはリクエストの完了前に実行され、レスポンスを変更できます。
基本的なミドルウェア:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Clone the request headers
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-custom-header', 'custom-value')
// Return response with modified headers
return NextResponse.next({
request: {
headers: requestHeaders,
},
})
}
export const config = {
matcher: '/api/:path*',
}
認証ミドルウェア:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
// Redirect to login if no token
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Redirect to dashboard if already logged in
if (token && request.nextUrl.pathname === '/login') {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
}
位置情報とリライト:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US'
// Rewrite based on country
if (country === 'GB') {
return NextResponse.rewrite(new URL('/gb' + request.nextUrl.pathname, request.url))
}
return NextResponse.next()
}
レート制限:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
})
export async function middleware(request: NextRequest) {
const ip = request.ip ?? '127.0.0.1'
const { success } = await ratelimit.limit(ip)
if (!success) {
return new Response('Too Many Requests', { status: 429 })
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*',
}
メタデータと SEO
Next.js には、ページのメタデータを定義するための Metadata API が用意されています。
静的メタデータ:
// app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn more about our company',
openGraph: {
title: 'About Us',
description: 'Learn more about our company',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
},
}
export default function AboutPage() {
return <h1>About Us</h1>
}
動的メタデータ:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
type Props = {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
type: 'article',
publishedTime: post.publishedAt,
},
}
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}
アイコン付きメタデータ:
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
default: 'My App',
template: '%s | My App',
},
description: 'My application description',
icons: {
icon: '/favicon.ico',
apple: '/apple-icon.png',
},
manifest: '/manifest.json',
}
画像の最適化
Next.js は Image コンポーネントを使用して、画像を自動的に最適化します。
基本的な画像:
import Image from 'next/image'
export default function Page() {
return (
<Image
src="/profile.jpg"
alt="Profile picture"
width={500}
height={500}
/>
)
}
リモート画像:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
],
},
}
// Component
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://images.unsplash.com/photo-1234567890"
alt="Photo"
width={800}
height={600}
priority // Load image with high priority
/>
)
}
コンテナーに合わせて表示:
import Image from 'next/image'
export default function Page() {
return (
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/background.jpg"
alt="Background"
fill
style={{ objectFit: 'cover' }}
/>
</div>
)
}
レスポンシブ画像:
import Image from 'next/image'
export default function Page() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1920}
height={1080}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
)
}
フォントの最適化
Next.js は next/font を使用して、フォントを自動的に最適化します。
Google Fonts:
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.className} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
}
ローカルフォント:
// app/layout.tsx
import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/my-font.woff2',
display: 'swap',
variable: '--font-my-font',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={myFont.variable}>
<body>{children}</body>
</html>
)
}
ワークフローパターン
新しいページの作成
appディレクトリに新しいフォルダーを作成しますpage.tsxファイルを追加します- デフォルトコンポーネントをエクスポートします
- 必要に応じて、レイアウト、読み込み、エラーファイルを追加します
// app/products/page.tsx
export default function ProductsPage() {
return <h1>Products</h1>
}
// app/products/layout.tsx
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<nav>Products Navigation</nav>
{children}
</div>
)
}
// app/products/loading.tsx
export default function Loading() {
return <div>Loading products...</div>
}
サーバーアクション
サーバーアクションを使用すると、クライアントコンポーネントからサーバー側のコードを直接実行できます。
サーバーアクションを使ったフォーム:
// 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')
}
// app/new-post/page.tsx
import { createPost } from '../actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<button type="submit">Create Post</button>
</form>
)
}
useTransition を使ったサーバーアクション:
// app/actions.ts
'use server'
export async function updateUser(userId: string, data: UserData) {
await db.user.update({
where: { id: userId },
data,
})
return { success: true }
}
// app/profile/page.tsx
'use client'
import { useTransition } from 'react'
import { updateUser } from '../actions'
export default function ProfilePage() {
const [isPending, startTransition] = useTransition()
const handleSubmit = (formData: FormData) => {
startTransition(async () => {
await updateUser('user-id', {
name: formData.get('name') as string,
})
})
}
return (
<form action={handleSubmit}>
<input name="name" />
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
</form>
)
}
認証パターン
ミドルウェアを使った基本認証:
// lib/auth.ts
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export async function getSession() {
const session = cookies().get('session')?.value
if (!session) return null
return await verifySession(session)
}
export async function requireAuth() {
const session = await getSession()
if (!session) {
redirect('/login')
}
return session
}
// app/dashboard/page.tsx
import { requireAuth } from '@/lib/auth'
export default async function DashboardPage() {
const session = await requireAuth()
return (
<div>
<h1>Welcome, {session.user.name}</h1>
</div>
)
}
// app/api/login/route.ts
import { cookies } from 'next/headers'
export async function POST(request: Request) {
const { email, password } = await request.json()
const user = await validateCredentials(email, password)
if (!user) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 })
}
const session = await createSession(user)
cookies().set('session', session, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 1 week
})
return Response.json({ success: true })
}
データベースパターン
サーバーコンポーネントでの Prisma:
// lib/db.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
// app/posts/page.tsx
import { prisma } from '@/lib/db'
async function getPosts() {
return await prisma.post.findMany({
orderBy: { createdAt: 'desc' },
include: { author: true },
})
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
<p>{post.content}</p>
</article>
))}
</div>
)
}
キャッシュ戦略
オンデマンドでの再検証:
// app/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function createPost(data: PostData) {
await db.post.create({ data })
// Revalidate specific path
revalidatePath('/posts')
// Or revalidate by tag
revalidateTag('posts')
}
// Fetch with cache tags
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
return res.json()
}
時間ベースの再検証:
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Revalidate every hour
})
return res.json()
}
ルートセグメントの設定:
// app/blog/page.tsx
export const revalidate = 3600 // Revalidate every hour
export const dynamic = 'force-static' // Force static generation
export const fetchCache = 'force-cache' // Force cache for all fetch requests
export default async function BlogPage() {
const posts = await getPosts()
return <div>{/* ... */}</div>
}
ベストプラクティス
1. デフォルトでサーバーコンポーネントを使用する
サーバーコンポーネントはデフォルトで使用され、パフォーマンスが向上します。必要な場合にのみクライアントコンポーネントを使用してください。
// ✅ Good - Server Component
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>
}
// ✅ Good - Client Component only when needed
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
2. 必要な場所でデータを取得する
データを props 経由で複数の階層に渡さないでください。必要とするコンポーネント内でデータを取得してください。
// ✅ Good - Fetch where needed
async function Header() {
const user = await getUser()
return <div>Welcome, {user.name}</div>
}
async function Posts() {
const posts = await getPosts()
return <div>{/* render posts */}</div>
}
export default function Page() {
return (
<>
<Header />
<Posts />
</>
)
}
3. 並列データ取得を使用する
可能な場合はデータを並列に取得して、読み込み時間を短縮してください。
// ✅ Good - Parallel fetching
export default async function Page() {
const [user, posts] = await Promise.all([
getUser(),
getPosts()
])
return <div>{/* ... */}</div>
}
4. 画像を最適化する
自動最適化のために、常に Image コンポーネントを使用してください。
// ✅ Good
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero"
width={1920}
height={1080}
priority
/>
// ❌ Bad
<img src="/hero.jpg" alt="Hero" />
5. SEO に Metadata API を使用する
すべてのページにメタデータを定義してください。
// ✅ Good
export const metadata = {
title: 'My Page',
description: 'Page description',
}
export default function Page() {
return <div>Content</div>
}
6. エラーバウンダリを実装する
エラー処理のために error.tsx ファイルを追加してください。
// app/error.tsx
'use client'
export default function Error({ error, reset }: { error: Error, reset: () => void }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}
7. 読み込み状態を使用する
読み込み UI のために loading.tsx ファイルを追加してください。
// app/loading.tsx
export default function Loading() {
return <div>Loading...</div>
}
8. API にルートハンドラーを実装する
pages ディレクトリの API ルートではなく、ルートハンドラーを使用してください。
// ✅ Good - app/api/users/route.ts
export async function GET(request: Request) {
const users = await db.user.findMany()
return Response.json(users)
}
9. 共通ロジックにミドルウェアを使用する
認証、リダイレクト、リライトをミドルウェアに実装してください。
// middleware.ts
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
10. フォントを最適化する
自動フォント最適化には next/font を使用してください。
// ✅ Good
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
)
}
一般的なパターン
ブログパターン
// app/blog/page.tsx
import Link from 'next/link'
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
})
return res.json()
}
export default async function BlogPage() {
const posts = await getPosts()
return (
<div>
<h1>Blog</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`)
if (!res.ok) return null
return res.json()
}
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post) => ({ slug: post.slug }))
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) return {}
return {
title: post.title,
description: post.excerpt,
}
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
E コマースパターン
// app/products/page.tsx
import { Suspense } from 'react'
import ProductGrid from './ProductGrid'
import Filters from './Filters'
export default function ProductsPage({
searchParams,
}: {
searchParams: { category?: string; sort?: string }
}) {
return (
<div>
<h1>Products</h1>
<Filters />
<Suspense fallback={<div>Loading products...</div>}>
<ProductGrid searchParams={searchParams} />
</Suspense>
</div>
)
}
// app/products/ProductGrid.tsx
async function getProducts(category?: string, sort?: string) {
const params = new URLSearchParams()
if (category) params.set('category', category)
if (sort) params.set('sort', sort)
const res = await fetch(`https://api.example.com/products?${params}`, {
next: { revalidate: 300 }
})
return res.json()
}
export default async function ProductGrid({
searchParams,
}: {
searchParams: { category?: string; sort?: string }
}) {
const products = await getProducts(searchParams.category, searchParams.sort)
return (
<div className="grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
// app/products/[id]/page.tsx
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`)
return res.json()
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
<AddToCartButton productId={product.id} />
</div>
)
}
ダッシュボードパターン
// app/dashboard/layout.tsx
import { requireAuth } from '@/lib/auth'
import Sidebar from './Sidebar'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await requireAuth()
return (
<div className="dashboard">
<Sidebar user={session.user} />
<main>{children}</main>
</div>
)
}
// app/dashboard/page.tsx
import { Suspense } from 'react'
import Stats from './Stats'
import RecentActivity from './RecentActivity'
import Chart from './Chart'
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading stats...</div>}>
<Stats />
</Suspense>
<div className="grid">
<Suspense fallback={<div>Loading chart...</div>}>
<Chart />
</Suspense>
<Suspense fallback={<div>Loading activity...</div>}>
<RecentActivity />
</Suspense>
</div>
</div>
)
}
まとめ
この Next.js 開発スキルでは、以下を扱います。
- App Router:ファイルベースのシステムによる最新のルーティング
- サーバーコンポーネント:パフォーマンス向上のための、デフォルトのサーバーサイドレンダリング
- データ取得:キャッシュと再検証に対応した拡張 fetch API
- ルーティング:動的ルート、キャッチオールルート、ルートグループ、並列ルート
- レイアウト:ネストされたレイアウト、テンプレート、レイアウトの構成
- 読み込み状態:Suspense による自動読み込み UI
- エラー処理:エラーバウンダリと not-found ページ
- API ルート:Web API を使ったルートハンドラー
- ミドルウェア:リクエストのインターセプトと変更
- メタデータ:Metadata API による SEO の最適化
- 画像の最適化:Image コンポーネントによる自動画像最適化
- フォントの最適化:next/font による自動フォント読み込み
- サーバーアクション:クライアントコンポーネントからのサーバー側の変更処理
- 認証:セッション管理と保護されたルート
- ベストプラクティス:パフォーマンス最適化、キャッシュ戦略、一般的なパターン
すべてのパターンは公式 Next.js ドキュメント(信頼スコア:10)に基づいており、最新の Next.js 13+ App Router 開発プラクティスを示しています。