NextJS App Router を初めて触る

NextJS App Router 周りの理解をClaude先生に聞いて深める
- App Router
- Server Components
- Client Components
- 従来の frontned + API Backendとの違い
先生
Claude 3.5 Sonnet

API Routes について詳しく知りたいです。
NextResponse とはなんでしょうか
API Routes は Next.js の機能の一つで、サーバーサイドの API エンドポイントを簡単に作成できる方法です。NextResponse は、これらの API Routes でレスポンスを構築するために使用される Next.js の組み込みユーティリティです。
API Routes と NextResponse について、より詳しく説明しましょう:
- API Routes:
API Routes を使用すると、Next.js アプリケーション内に独自の API エンドポイントを作成できます。これらは app/api
ディレクトリ内に配置され、サーバーサイドで実行されます。
主な特徴:
- RESTful API エンドポイントの作成が可能
- サーバーサイドのロジックを実装できる
- データベースへの直接アクセスや外部 API の呼び出しが可能
- クライアントサイドの JavaScript から fetch で呼び出せる
- NextResponse:
NextResponse は next/server
からインポートされるオブジェクトで、API Routes でレスポンスを構築するために使用されます。
主な特徴:
- さまざまな型のレスポンス(JSON、テキスト、リダイレクトなど)を簡単に作成できる
- ヘッダーの設定、ステータスコードの指定が可能
- Cookie の設定や読み取りが可能
では、API Routes と NextResponse の使用例を見てみましょう:
// app/api/users/route.js
import { NextResponse } from 'next/server';
// GET リクエストの処理
export async function GET(request) {
// URLからクエリパラメータを取得
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
// 仮のユーザーデータ
const users = [
{ id: '1', name: 'John Doe' },
{ id: '2', name: 'Jane Smith' }
];
if (id) {
const user = users.find(u => u.id === id);
return user
? NextResponse.json(user)
: NextResponse.json({ error: 'User not found' }, { status: 404 });
}
return NextResponse.json(users);
}
// POST リクエストの処理
export async function POST(request) {
const body = await request.json();
// 新しいユーザーを作成するロジック(仮の実装)
const newUser = { id: Date.now().toString(), ...body };
return NextResponse.json(newUser, { status: 201 });
}
// DELETE リクエストの処理
export async function DELETE(request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ error: 'User ID is required' }, { status: 400 });
}
// ユーザー削除のロジック(仮の実装)
return NextResponse.json({ message: `User ${id} deleted` }, { status: 200 });
}
// その他のHTTPメソッド(PUT, PATCH等)も同様に実装可能
この例では、以下のような特徴が示されています:
-
異なる HTTP メソッド(GET, POST, DELETE)に対応する関数を export しています。
-
NextResponse.json()
を使用して JSON レスポンスを返しています。 -
ステータスコードを指定して、適切なレスポンスを返しています(例: 201 for Created, 404 for Not Found)。
-
クエリパラメータを使用してリクエストを処理しています(GET と DELETE メソッド)。
-
リクエストボディからデータを取得しています(POST メソッド)。
NextResponse の他の使用方法:
// リダイレクト
return NextResponse.redirect(new URL('/new-page', request.url));
// プレーンテキストレスポンス
return NextResponse.text('Hello, World!');
// カスタムヘッダー付きレスポンス
return NextResponse.json({ message: 'Success' }, {
headers: {
'X-Custom-Header': 'some-value'
}
});
// Cookie の設定
const response = NextResponse.json({ message: 'Logged in' });
response.cookies.set('token', 'your-auth-token');
return response;
API Routes と NextResponse を使用することで、フロントエンドとバックエンドのロジックを同じ Next.js プロジェクト内に統合でき、開発の効率化とコードの一貫性を高めることができます。また、サーバーレス関数としてデプロイすることも可能で、スケーラブルなアプリケーション開発に適しています。

"server" と "client" について詳しく理解したいです。 "server components" はブラウザ上で仮想的に動いているのか、それとも、ホスティングサーバーでサーバが別に起動しているのか、そうだとしたらAPIサーバーと何が違うのか。
"server" と "client" の概念、特に Next.js のコンテキストでは少し複雑になります。詳しく説明しましょう。
- Server Components
Server Components は実際にサーバー上で実行されます。これは「ホスティングサーバー」と呼ばれるものです。ブラウザ上で仮想的に動作しているわけではありません。
主な特徴:
- サーバー上でレンダリングされ、結果のHTMLがクライアントに送信されます。
- JavaScriptバンドルサイズを削減し、初期ロード時間を改善します。
- データベースやファイルシステムに直接アクセスできます。
- APIキーなどの機密情報を安全に扱えます。
- Client Components
Client Components はブラウザ上で実行されます。
主な特徴:
- ブラウザ上でレンダリングされ、インタラクティブな機能を提供します。
- ユーザーイベント(クリックなど)に反応できます。
- クライアントサイドの状態を管理できます。
- サーバーの種類
Next.js のサーバーについて、いくつかの異なる「サーバー」の概念があります:
a) 開発サーバー:
-
npm run dev
で起動するローカルの開発サーバー。 - Server Components とAPI Routesの両方を処理します。
b) 本番サーバー:
- 従来のNode.jsサーバーで、
npm run start
で起動します。 - Server ComponentsとAPI Routesを処理します。
c) エッジランタイム:
- Vercelなどのプラットフォームで提供される軽量なサーバー環境。
- Server ComponentsとAPI Routesの一部を実行できます。
d) Static Site Generation (SSG):
- ビルド時にページを生成し、CDNで配信します。
- Server Componentsの結果を事前に生成します。
- API サーバーとの違い
Next.js の Server Components とAPI Routesは、従来のAPIサーバーとは以下の点で異なります:
- 統合:Server ComponentsとAPI Routesは同じNext.jsアプリケーション内に統合されています。
- 目的:Server Componentsは主にUIレンダリングのためのもので、API Routesはデータ操作やバックエンドロジックのためのものです。
- 実行タイミング:Server Componentsはページリクエスト時に実行され、API Routesは明示的なAPIコール時に実行されます。
実際の動作を理解するために、簡単な例を見てみましょう:
// app/api/user/route.js (API Route)
import { NextResponse } from 'next/server';
export async function GET() {
// この関数はサーバー上で実行されます
const userData = { name: 'John Doe', email: 'john@example.com' };
return NextResponse.json(userData);
}
// app/page.js (Server Component)
async function getUserData() {
// この関数はサーバー上で実行されます
const res = await fetch('http://localhost:3000/api/user');
return res.json();
}
export default async function HomePage() {
// この関数コンポーネント全体がサーバー上で実行されます
const userData = await getUserData();
return (
<div>
<h1>Welcome, {userData.name}</h1>
<ClientSideComponent email={userData.email} />
</div>
);
}
// app/ClientSideComponent.js (Client Component)
'use client'
import { useState } from 'react';
export default function ClientSideComponent({ email }) {
// この関数コンポーネントはブラウザ上で実行されます
const [isVisible, setIsVisible] = useState(false);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>Toggle Email</button>
{isVisible && <p>Email: {email}</p>}
</div>
);
}
この例では:
-
API Route (
/api/user
) はサーバー上で実行され、ユーザーデータを返します。 -
Server Component (
HomePage
) もサーバー上で実行されます:-
getUserData
関数を呼び出してAPI Routeからデータを取得します。 - データを使ってHTMLを生成します。
- 生成されたHTMLがクライアントに送信されます。
-
-
Client Component (
ClientSideComponent
) はブラウザ上で実行されます:- Server Componentから渡されたprops(この場合は
email
)を受け取ります。 - ユーザーのインタラクション(ボタンクリック)に応じて状態を変更します。
- Server Componentから渡されたprops(この場合は
このように、Server ComponentsとAPI Routesはサーバー上で動作し、Client Componentsはブラウザ上で動作します。Next.jsはこれらを seamlessに統合し、効率的なアプリケーション開発を可能にしています。

従来の frontend + backend 構成では、 frontend がUIのみを担当し、 backend である API Server からデータを取得することが多かったと思います。
App Router では、 Client Components + Server Components + API Routes (+ Backend; API Server) の構成になるという理解で良いですか?
はい、その理解は正確です。Next.js の App Router を使用した現代的なアプリケーション構成は、従来のフロントエンド+バックエンド構成を進化させたものと言えます。
従来の構成と App Router を使用した構成を比較してみましょう:
-
従来の構成:
- Frontend (React等): UIレンダリング、状態管理
- Backend (API Server): データ処理、ビジネスロジック、データベース操作
-
Next.js App Router の構成:
- Client Components: クライアントサイドのインタラクティブUI
- Server Components: サーバーサイドレンダリング、データフェッチ
- API Routes: 軽量なAPIエンドポイント
- Backend (必要に応じて): 複雑なビジネスロジック、大規模データ処理
この新しい構成の利点と特徴を詳しく見ていきましょう:
Next.js App Router の各コンポーネントの役割:
-
Client Components:
- ユーザーインタラクションの処理
- クライアントサイドの状態管理
- 動的なUI更新
-
Server Components:
- サーバーサイドレンダリング
- データフェッチング(API Routes, 外部API, データベースから)
- 初期ページロードの最適化
-
API Routes:
- 軽量なAPIエンドポイントの提供
- サーバーサイドのロジック実行
- 外部APIやデータベースとの通信
-
Backend (オプション):
- 複雑なビジネスロジックの実装
- 大規模なデータ処理
- マイクロサービスアーキテクチャの一部として機能
この新しいアーキテクチャの主な利点:
-
パフォーマンスの向上:
- Server Componentsによる初期ロード時間の短縮
- 必要に応じたクライアントサイドのハイドレーション
-
開発の効率化:
- フロントエンドとバックエンドのロジックを同じプロジェクト内で管理
- シームレスなサーバーサイドとクライアントサイドの統合
-
スケーラビリティ:
- 必要に応じて外部バックエンドと統合可能
- サーバーレスデプロイメントのサポート
-
SEO対応:
- Server Componentsによる優れたSEOパフォーマンス
-
セキュリティ:
- サーバーサイドでの機密情報の安全な取り扱い
実際の実装例を見てみましょう:
// app/api/users/[id]/route.js (API Route)
import { NextResponse } from 'next/server';
export async function GET(request, { params }) {
const id = params.id;
// データベースからユーザー情報を取得(仮想的な実装)
const user = await fetchUserFromDatabase(id);
return NextResponse.json(user);
}
// app/users/[id]/page.js (Server Component)
async function getUserData(id) {
const res = await fetch(`http://localhost:3000/api/users/${id}`);
return res.json();
}
export default async function UserPage({ params }) {
const userData = await getUserData(params.id);
return (
<div>
<h1>{userData.name}'s Profile</h1>
<UserInteractions userId={userData.id} />
</div>
);
}
// app/users/[id]/UserInteractions.js (Client Component)
'use client'
import { useState } from 'react';
export default function UserInteractions({ userId }) {
const [likes, setLikes] = useState(0);
const handleLike = async () => {
setLikes(likes + 1);
await fetch(`/api/users/${userId}/like`, { method: 'POST' });
};
return (
<button onClick={handleLike}>Like ({likes})</button>
);
}
この例では:
- API Route がデータベースからユーザー情報を取得します。
- Server Component がAPI Routeを使ってデータをフェッチし、初期HTMLをレンダリングします。
- Client Component がユーザーインタラクション(「いいね」ボタン)を処理します。
この構成により、サーバーサイドレンダリングの利点(高速な初期ロード、SEO)とクライアントサイドのインタラクティブ性を両立させつつ、必要に応じて外部バックエンドと連携することができます。Next.js App Router は、これらのコンポーネントを効果的に組み合わせ、現代的なウェブアプリケーション開発のニーズに応えるアーキテクチャを提供しています。

API Routes は Database のアクセスが可能になる場合、コード上は NextJS の中に DB Secret などが含まれる可能性がありますよね。もし環境変数で同じように使った場合意図せず frontend に露出する可能性はないのですか?
非常に重要な質問です。セキュリティは常に最優先事項であり、特にデータベースの認証情報などの機密情報の扱いには細心の注意が必要です。Next.js の API Routes と環境変数の使用に関して、以下の点を詳しく説明します:
- API Routes とデータベースアクセス
API Routes は確かにデータベースに直接アクセスすることが可能で、そのためにデータベースの認証情報(DB Secret など)が必要になります。
- 環境変数の使用
Next.js では、環境変数を使用して機密情報を管理することが推奨されています。しかし、全ての環境変数がフロントエンドに露出しないわけではありません。
- サーバーサイド vs クライアントサイドの環境変数
Next.js は環境変数を2つのカテゴリに分けています:
a) サーバーサイドのみの環境変数
b) クライアントサイドでも利用可能な環境変数
- 環境変数の露出を防ぐ方法
Next.js では、環境変数名の先頭に NEXT_PUBLIC_
をつけない限り、その変数はサーバーサイドでのみ利用可能で、クライアントサイド(ブラウザ)には露出しません。
例を見てみましょう:
# .env
DATABASE_URL=postgres://username:password@localhost:5432/mydb
NEXT_PUBLIC_API_URL=https://api.example.com
# pages/api/users.js (API Route)
import { pool } from '../../lib/db'
export default async function handler(req, res) {
const client = await pool.connect()
try {
const result = await client.query('SELECT * FROM users')
res.status(200).json(result.rows)
} finally {
client.release()
}
}
# pages/index.js (Client-side component)
import { useState, useEffect } from 'react'
export default function Home() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data))
}, [])
return (
<div>
<h1>Users</h1>
<p>API URL: {process.env.NEXT_PUBLIC_API_URL}</p>
{users.map(user => <div key={user.id}>{user.name}</div>)}
</div>
)
}
このコード例から以下のポイントが分かります:
-
DATABASE_URL
はNEXT_PUBLIC_
プレフィックスがないため、クライアントサイドには露出しません。API Route 内でのみ安全に使用できます。 -
NEXT_PUBLIC_API_URL
は クライアントサイドでも利用可能です。これは公開しても問題ない情報(例:公開APIのURL)に使用します。 -
API Route (
pages/api/users.js
) はサーバーサイドでのみ実行されるため、ここでDATABASE_URL
を安全に使用できます。 -
クライアントサイドのコンポーネント (
pages/index.js
) では、DATABASE_URL
にアクセスできません。 -
セキュリティのベストプラクティス:
- 機密情報(DB認証情報など)は必ず
NEXT_PUBLIC_
プレフィックスなしの環境変数に格納する。 - API Routes 内でのみデータベースアクセスを行い、クライアントサイドコンポーネントではAPI Routesを介してデータを取得する。
- 本番環境では、環境変数をホスティングプラットフォーム(Vercel, Netlifyなど)のセキュアな環境変数管理システムを使用して設定する。
-
.env
ファイルを.gitignore
に追加し、リポジトリにコミットしないようにする。
- 機密情報(DB認証情報など)は必ず
-
追加の注意点:
- ビルド時に環境変数が評価されるため、環境変数を変更した場合は再ビルドが必要。
- クライアントサイドで使用する環境変数(
NEXT_PUBLIC_
プレフィックス付き)の値は、ブラウザの開発者ツールで見ることができるため、本当に公開して問題ない情報のみを使用する。
このように、Next.js の環境変数システムを正しく使用すれば、データベースの認証情報などの機密情報がフロントエンドに露出するリスクを最小限に抑えることができます。ただし、常にセキュリティを意識し、定期的にコードレビューを行うことが重要です。

ある server component が client component を import して使うことはできますか?
仮にできるならば、 client component はブラウザでレンダリングすること、 server component はサーバでレンダリングすることが矛盾してしまうように思いました。
優れた質問です。この点は多くの開発者が混乱しがちな部分です。Server ComponentがClient Componentをインポートして使用することは可能です。しかし、その動作は少し複雑で、直感に反するように感じられるかもしれません。
詳しく説明しましょう:
- Server ComponentがClient Componentをインポートする
Server ComponentはClient Componentをインポートして使用することができます。しかし、その処理は以下のように行われます:
- Server Componentはサーバー上でレンダリングされます。
- Client Componentの「プレースホルダー」がサーバーレンダリングの結果に含まれます。
- クライアント側で、このプレースホルダーが実際のClient Componentで置き換えられます(ハイドレーション)。
- レンダリングプロセス
このプロセスを段階的に見てみましょう:
a) サーバーサイド:
- Server Componentがレンダリングされます。
- Client Componentの位置に特別なマーカー(プレースホルダー)が挿入されます。
b) クライアントサイド:
- サーバーからのレスポンスを受け取ります。
- JavaScriptが読み込まれると、Client Componentのプレースホルダーが実際のコンポーネントで置き換えられます。
- 例示
この概念を理解するために、具体的な例を見てみましょう:
// app/page.js (Server Component)
import ClientCounter from './ClientCounter'
export default function Page() {
return (
<div>
<h1>Welcome to my app</h1>
<p>This is a server component.</p>
<ClientCounter initialCount={5} />
</div>
)
}
// app/ClientCounter.js (Client Component)
'use client'
import { useState } from 'react'
export default function ClientCounter({ initialCount }) {
const [count, setCount] = useState(initialCount)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
この例での実際の処理フローは以下のようになります:
-
サーバーサイド:
-
Page
コンポーネント(Server Component)がサーバーでレンダリングされます。 -
ClientCounter
の位置にプレースホルダーが挿入されます。 - 初期HTMLが生成されクライアントに送信されます。
-
-
クライアントサイド:
- ブラウザが初期HTMLを受け取り表示します。
- JavaScriptが読み込まれると、
ClientCounter
のプレースホルダーが実際のコンポーネントで置き換えられます。 -
ClientCounter
がクライアントサイドでレンダリングされ、インタラクティブになります。
-
利点
この仕組みには以下のような利点があります:
- 初期ページロードが高速:Server Componentの部分が即座に表示されます。
- インタラクティブ性の維持:Client Componentが必要な部分のみがクライアントサイドでレンダリングされます。
- 最適なパフォーマンス:静的な部分はサーバーで、動的な部分はクライアントで処理されます。
- 注意点
- Server ComponentからClient Componentへのデータの受け渡しは、propsを通してのみ行えます。
- Client ComponentをServer Componentの中で条件付きでレンダリングする場合、その条件分岐もサーバーサイドで行われます。
- レンダリングの視覚化
レンダリングプロセスをより明確に理解するために、視覚的な表現を見てみましょう:
この図は、Server ComponentとClient Componentが協調して動作する様子を示しています。
結論として、Server ComponentがClient Componentを含むことは可能であり、矛盾するものではありません。Next.jsはこの仕組みを通じて、サーバーサイドレンダリングの利点(高速な初期ロード、SEOの改善)とクライアントサイドレンダリングの利点(インタラクティブ性)を組み合わせています。これにより、パフォーマンスとユーザー体験の両方を最適化することができます。

認証について聞きたいです。 Server Components がメインになると、そちらで認証が行われるようになるかと思います。 レンダーされた Components をブラウザで受け取るかと思います。そもそも、送信されてくる Server Components からの通信内容は、ブラウザで、どのように認証を行うのでしょうか?
素晴らしい質問です。認証は Web アプリケーションの重要な部分であり、Server Components の導入によって認証の仕組みが変わる部分があります。この複雑なトピックを段階的に説明していきましょう。
- Server Components と認証の基本概念
Server Components を使用する場合、主な認証処理はサーバーサイドで行われます。これには以下のような利点があります:
- セキュリティの向上:認証ロジックがクライアントに露出しない
- パフォーマンスの向上:認証済みコンテンツをサーバーサイドで直接レンダリングできる
- 認証の流れ
一般的な認証フローは次のようになります:
a) ユーザーがログイン情報を提供
b) サーバーが認証を行い、セッションまたはトークンを生成
c) セッション/トークンがクライアントに送信され、通常はクッキーに保存
d) 以降のリクエストでこのセッション/トークンが使用される
- Server Components と認証の連携
Server Components を使用する場合、認証状態の管理と検証は主にサーバーサイドで行われます:
a) リクエストが来るたびに、サーバーはセッション/トークンを検証
b) 認証が成功すれば、適切な Server Component がレンダリングされる
c) 認証が失敗した場合、未認証ユーザー向けのコンテンツまたはリダイレクトが返される
- クライアントサイドでの認証状態の管理
ブラウザで Server Components からの通信内容の認証を直接行うわけではありません。代わりに、以下のような方法で認証状態を管理します:
a) HTTP Only Cookies:
- セキュアな HTTP Only クッキーにセッション ID やトークンを保存
- これらは自動的に各リクエストに含まれる
b) クライアントサイドの状態管理:
- 認証状態を示すフラグをクライアントサイドのステート(React の state など)で管理
- この状態は Server Component からの初期データに基づいて設定される
- 実装例
次に、Next.js での認証の実装例を見てみましょう:
// app/api/login/route.js
import { cookies } from 'next/headers'
import { sign } from 'jsonwebtoken'
export async function POST(request) {
const { username, password } = await request.json()
// ここで実際の認証ロジックを実装
if (username === 'user' && password === 'password') {
const token = sign({ username }, process.env.JWT_SECRET, { expiresIn: '1h' })
cookies().set('auth-token', token, { httpOnly: true, secure: true })
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
return new Response(JSON.stringify({ success: false }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
})
}
// lib/auth.js
import { cookies } from 'next/headers'
import { verify } from 'jsonwebtoken'
export function getUser() {
const token = cookies().get('auth-token')
if (!token) return null
try {
return verify(token.value, process.env.JWT_SECRET)
} catch {
return null
}
}
// app/page.js (Server Component)
import { getUser } from '../lib/auth'
export default function Home() {
const user = getUser()
if (!user) {
return <div>Please log in</div>
}
return <div>Welcome, {user.username}!</div>
}
// app/login/page.js (Client Component for login form)
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function Login() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const router = useRouter()
const handleSubmit = async (e) => {
e.preventDefault()
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
if (res.ok) {
router.push('/')
router.refresh() // このテクニックでサーバーコンポーネントを再フェッチ
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
)
}
この例では:
-
ログイン API (
app/api/login/route.js
) がユーザー認証を処理し、JWT トークンを生成してクッキーに設定します。 -
getUser
関数 (lib/auth.js
) がサーバーサイドでトークンを検証し、ユーザー情報を取得します。 -
Server Component (
app/page.js
) がgetUser
を使用して認証状態を確認し、適切なコンテンツをレンダリングします。 -
ログインフォーム (
app/login/page.js
) は Client Component として実装され、ユーザー入力を処理し API を呼び出します。 -
セキュリティ上の考慮事項
- CSRF 対策:適切な CSRF トークンの使用
- XSS 対策:HttpOnly フラグ付きのクッキーの使用
- HTTPS:全ての通信で HTTPS を使用
- 認証フローの視覚化
認証プロセスをより明確に理解するために、視覚的な表現を見てみましょう:
結論として、Server Components を使用する場合、認証の主要な部分はサーバーサイドで処理されます。ブラウザは主に認証済みの状態(クッキーなど)を保持し、それを各リクエストで送信する役割を果たします。Server Components は、この認証状態に基づいて適切なコンテンツをレンダリングし、クライアントに送信します。この方法により、セキュリティが向上し、かつパフォーマンスも最適化されます。

page routers の開発に慣れている人が、段階的に App Router に移行するには、特に client components や server components のバランスの観点でどのようにしていくと良いか?
例えば、今まで行っていた hook による API Server の通信などの扱いなどです。
Page Router から App Router への段階的な移行は、多くの開発者が直面する課題です。特に Client Components と Server Components のバランス、そして既存の Hooks を使用した API 通信の扱いは重要なポイントです。以下に、段階的な移行のアプローチと、特に注意すべき点を説明します。
- 段階的な移行のアプローチ
- Client Components と Server Components のバランス
a) まずは Client Components として移行:
- 既存のページを
app
ディレクトリに移動し、'use client'
ディレクティブを追加します。 - これにより、既存のコードの動作を維持しながら、App Router の構造に慣れることができます。
b) 徐々に Server Components に変換:
- データフェッチングや静的な UI 部分を Server Components に移行します。
- インタラクティブな部分は Client Components のままにします。
c) コンポーネントの分割:
- 大きなコンポーネントを、Server Component と Client Component に分割します。
- 例: フォームの静的な部分を Server Component に、入力処理を Client Component に分けます。
- Hooks と API 通信の扱い
a) Server Components でのデータフェッチ:
-
useEffect
やuseSWR
で行っていたデータフェッチを、Server Components の非同期関数に移行します。 - これにより、初期ロード時のパフォーマンスが向上します。
b) Client Components での動的データフェッチ:
- ユーザーインタラクションに基づくデータフェッチは、Client Components 内で従来の Hooks を使用して行います。
c) React Query や SWR の活用:
- これらのライブラリは App Router と互換性があり、Client Components 内でのデータ管理に有効です。
- 具体的な移行例
以下に、Page Router から App Router への移行例を示します:
// Before (pages/users/[id].js)
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
export default function UserPage() {
const router = useRouter()
const { id } = router.query
const [user, setUser] = useState(null)
useEffect(() => {
if (id) {
fetch(`/api/users/${id}`)
.then(res => res.json())
.then(data => setUser(data))
}
}, [id])
if (!user) return <div>Loading...</div>
return <div>User: {user.name}</div>
}
// After (app/users/[id]/page.js)
async function getUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`)
if (!res.ok) throw new Error('Failed to fetch user')
return res.json()
}
export default async function UserPage({ params }) {
const user = await getUser(params.id)
return <div>User: {user.name}</div>
}
// After (app/users/[id]/loading.js)
export default function Loading() {
return <div>Loading...</div>
}
// After (app/users/[id]/error.js)
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
この例では:
- データフェッチを Server Component の非同期関数に移行しています。
- ローディング状態を専用の
loading.js
ファイルで処理しています。 - エラーハンドリングを
error.js
で行っています。
- 注意点とベストプラクティス
a) パフォーマンスの最適化:
- Server Components を使用してサーバーサイドでのデータフェッチを最大限活用します。
- 必要な場合のみ Client Components を使用し、JavaScript のバンドルサイズを最小限に抑えます。
b) 段階的なリファクタリング:
- 一度にすべてを移行するのではなく、ページやコンポーネントごとに徐々に移行します。
- 各段階でテストを行い、機能が正しく動作することを確認します。
c) 状態管理の見直し:
- グローバルな状態管理(Redux など)の使用を再評価し、必要に応じて Server Components でのデータフェッチに置き換えます。
d) TypeScript の活用:
- 移行過程で TypeScript を導入または強化し、型安全性を向上させます。
e) ルーティングの違いに注意:
-
next/router
の代わりにnext/navigation
を使用します。 - 動的ルーティングのパラメータの扱いが変わることに注意します。
- 移行のチェックリスト
最後に、移行のための簡単なチェックリストを提供します:
# App Router Migration Checklist
- [ ] ディレクトリ構造を `app/` に移行
- [ ] 各ページを Server Component として再実装
- [ ] データフェッチを Server Components に移行
- [ ] インタラクティブな部分を Client Components として分離
- [ ] ルーティングを `next/navigation` に更新
- [ ] レイアウトコンポーネントを作成 (`layout.js`)
- [ ] ローディング状態を `loading.js` で処理
- [ ] エラーハンドリングを `error.js` で実装
- [ ] メタデータとSEOの設定を更新
- [ ] パフォーマンスのテストと最適化
- [ ] アクセシビリティの確認
このアプローチに従うことで、Page Router から App Router への移行をスムーズに行い、Next.js の新機能を最大限に活用できるようになります。移行プロセスは段階的に行い、各ステップでアプリケーションの機能とパフォーマンスを確認することが重要です。

App Routers で既存の hook を使えますか?そのために必要なことは?
はい、App Router でも既存の Hooks を使用できます。ただし、使用方法や適用場所に関して、いくつかの重要な点に注意する必要があります。
- Client Components での Hook の使用
App Router では、Hooks を使用するためには、そのコンポーネントを Client Component として明示的に宣言する必要があります。
'use client';
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Counter updated:', count);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
- Server Components との統合
Server Components 内で直接 Hooks を使用することはできませんが、Client Components をインポートして使用することは可能です。
// app/page.js (Server Component)
import Counter from './Counter';
export default function Page() {
return (
<div>
<h1>Welcome to my app</h1>
<Counter />
</div>
);
}
- データフェッチング Hooks の代替
useEffect
を使用したデータフェッチングは、Server Components では直接使用できません。代わりに、以下のアプローチを検討してください:
a) Server Components での非同期データフェッチ:
// app/users/page.js
async function getUsers() {
const res = await fetch('https://api.example.com/users');
if (!res.ok) throw new Error('Failed to fetch users');
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
b) Client Components での SWR または React Query の使用:
'use client';
import useSWR from 'swr';
const fetcher = (...args) => fetch(...args).then(res => res.json());
export default function UserList() {
const { data, error } = useSWR('/api/users', fetcher);
if (error) return <div>Failed to load</div>;
if (!data) return <div>Loading...</div>;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
- カスタム Hooks の使用
カスタム Hooks も Client Components 内で通常通り使用できます。
'use client';
import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState([0, 0]);
useEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
return size;
}
export default function WindowSizeDisplay() {
const [width, height] = useWindowSize();
return <div>Window size: {width} x {height}</div>;
}
- Context API の使用
Context API も Client Components 内で使用できますが、Provider は Client Component である必要があります。
'use client';
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// Usage in a Client Component
export default function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme (Current: {theme})
</button>
);
}
- 注意点とベストプラクティス
a) パフォーマンスの考慮:
- Client Components(とそれに含まれる Hooks)の使用は、必要な場合のみにとどめましょう。
- 可能な限り、静的または動的なServer Componentsを使用してパフォーマンスを最適化しましょう。
b) ハイドレーションの理解:
- Client Components は、サーバーからの初期 HTML レンダリング後にハイドレーションされます。
- これにより、初期レンダリングと JavaScript の実行の間に不一致が生じる可能性があることを理解しておく必要があります。
c) Server Components との境界を意識:
- データフェッチングや重い計算は可能な限り Server Components で行い、UI の動的な部分のみを Client Components に委ねるようにしましょう。
d) TypeScript の活用:
- 型安全性を確保するために、Hooks と Client Components で TypeScript を使用することを検討しましょう。
以上の点に注意しながら既存の Hooks を App Router に統合することで、Server Components の利点を活かしつつ、必要な箇所で React の強力な機能を利用することができます。移行プロセスでは、各コンポーネントの役割と責任を慎重に検討し、適切に Server Components と Client Components を使い分けることが重要です。