Next.jsのルーティングの特徴
Next.jsのルーティングは、ファイルベースが最大の特徴。Reactだと、react-router-dom
といったライブラリを入れて手動でルートを定義しないといけませんが、Next.jsならファイルを置くだけでルートが自動で設定されます。Next.jsには大きく分けて Page Router と App Router の二つのルーティング方法が存在します。この記事ではNext.js 13で導入された App Router(/app
ディレクトリを使う方式)について記述していきます。
Reactとの違い
-
React:
<Route path="/about" component={About} />
みたいにルートを手動で書く。 - Next.js: ファイルを作るだけ。コードを書くよりフォルダ整理がメイン。
フォルダ構成とルーティングの例
App Routerを使うと、/app
配下のフォルダとファイルがそのままルーティングになります。具体例は以下になります。
サンプルフォルダ構成
app/
├── layout.js # ルート全体のレイアウト
├── page.js # ホーム (/)
├── loading.js # ルート全体のローディングUI
├── error.js # ルート全体のエラーUI
├── about/
│ └── page.js # /about
├── products/
│ ├── page.js # /products
│ ├── [id]/
│ │ └── page.js # /products/[id](例: /products/1)
└── api/
└── hello/
└── route.js # /api/hello
どう動く?
-
/
:/app/page.js
が対応。そのフォルダのメイン画面となる。 -
/about
:/app/about/page.js
が表示される。 -
/products
:/app/products/page.js
が表示される。 -
/products/1
:/app/products/[id]/page.js
で動的ルート。 -
/api/hello
:/app/api/hello/route.js
でAPIエンドポイント。
このように/app直下のフォルダ名と page.js
の位置で、URLが決まります。
App Routerのファイル規定について
Next.jsのApp Routerでは、特定の役割を持つ 規定ファイル が定義されています。これを使うと、ルーティングやUIの挙動を自動で制御できます。規定されたファイル名以外は認識されません。
主な規定ファイル
-
page.js
- 役割: そのフォルダのメインとなる画面。これがないと404になる。App Routerではデフォルトでサーバーコンポーネントとして動作し、サーバー側でレンダリングされる。
-
例:
→
// app/about/page.js export default function About() { return <h1>Aboutページです</h1>; }
/about
で表示。
-
layout.js
- 役割: ルート全体や特定フォルダに適用される共通レイアウト。サーバーコンポーネントとして動作。
-
例:
→ 全てのページにヘッダーとフッターが付く。
// app/layout.js export default function RootLayout({ children }) { return ( <html> <body> <header>共通ヘッダー</header> {children} <footer>共通フッター</footer> </body> </html> ); }
-
loading.js
- 役割: ページ読み込み中のローディングUI。サーバー側で動作し、データ取得中の表示を簡単に実現。
-
例:
→ データ取得中などに表示。
// app/loading.js export default function Loading() { return <p>読み込み中...</p>; }
-
error.js
-
役割: エラー時の画面。ただし、エラー処理でユーザー操作(例: リトライボタン)が必要な場合、クライアントサイドの動作が求められるため、
"use client"
を追加してクライアントコンポーネントとして定義する。 -
例:
→ エラーキャッチしてユーザーに優しく。
// app/error.js "use client"; // クライアントサイドで動作させる export default function Error({ error, reset }) { return ( <div> <p>エラー: {error.message}</p> <button onClick={reset}>リトライ</button> </div> ); }
"use client"
がないと、イベントハンドラ(onClick
)が動作しない。
-
役割: エラー時の画面。ただし、エラー処理でユーザー操作(例: リトライボタン)が必要な場合、クライアントサイドの動作が求められるため、
-
not-found.js
- 役割: 404エラー時の画面。
-
例:
→ ルートがないときに表示。
// app/not-found.js export default function NotFound() { return <h1>404 - ページが見つかりません</h1>; }
-
route.js
-
役割: APIエンドポイントを定義。Pages Routerの
pages/api
に代わるもの。HTTPメソッド(GET
,POST
など)に対応する関数をエクスポートし、サーバーサイドで動作するAPIを作成。データベース操作や外部API呼び出しをサーバー側で処理するのに便利。 -
例:
→
// app/api/hello/route.js export async function GET() { return Response.json({ message: "Hello Next" }); } export async function POST(request) { const body = await request.json(); return Response.json({ received: body }); }
/api/hello
にGETリクエストを送ると{ "message": "Hello Next" }
が返り、POSTリクエストでデータを送信すると受信データが返される。
-
役割: APIエンドポイントを定義。Pages Routerの
-
命名厳守:
page.js
じゃないとルートにならないし、layout.js
じゃないとレイアウトとして認識されない。 -
階層対応: フォルダごとに個別の
loading.js
やerror.js
を置けば、その範囲だけで有効。
"use client"
とは?
App Routerでは、デフォルトで全てのコンポーネントがサーバーコンポーネントとして動作します。サーバーコンポーネントはサーバー側でレンダリングされ、クライアントに送信されるのはHTMLのみで、JavaScriptの負荷が軽減されます。しかし、以下のような場合にはクライアントサイドでの動作が必要になります:
- 状態管理(
useState
)やエフェクト(useEffect
)を使う場合。 - ブラウザのイベント(
onClick
,onChange
など)を扱う場合。 - ブラウザAPI(
window
,localStorage
など)にアクセスする場合。
このようなケースでは、ファイル冒頭に"use client"
を追加することで、そのコンポーネントをクライアントコンポーネントとして明示的に指定します。
-
例: ボタンクリックでカウンターを増やすコンポーネント:
// app/counter/page.js "use client"; import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>カウント: {count}</p> <button onClick={() => setCount(count + 1)}>増やす</button> </div> ); }
→
"use client"
がないと、useState
やonClick
がサーバー側では動作せずエラーになる。 -
補足: サーバーコンポーネント内でクライアントコンポーネントをインポートして混在させることも可能。ただし、クライアントコンポーネントはサーバー側で事前レンダリングされないため、パフォーマンスを考慮して使い分ける必要があります。
動的ルートの作り方
URLに変数(パラメータ)を含めたいとき、動的ルートが便利です。フォルダ名に [ ]
を使ってパラメータを定義します。
例: 商品詳細ページ
-
構成:
/app/products/[id]/page.js
-
コード:
// app/products/[id]/page.js export default function ProductPage({ params }) { return <h1>商品ID: {params.id}</h1>; }
-
動作:
-
/products/1
→ "商品ID: 1" -
/products/abc
→ "商品ID: abc"
-
パラメータの取得
-
params
オブジェクトで動的値を受け取れる。 - 複数パラメータもOK:
[category]/[id]
ならparams.category
とparams.id
。
データ取得と組み合わせ
-
例: サーバーコンポーネントで直接データ取得。
// app/products/[id]/page.js export default async function ProductPage({ params }) { const res = await fetch(`https://api.example.com/products/${params.id}`); const product = await res.json(); return <h1>{product.name}</h1>; }
Pages Routerと比較した際の App Routerのメリット
1. サーバーコンポーネントのデフォルト採用
Pages Router:
- コンポーネントは基本的にクライアントサイドで動作。サーバーサイドレンダリング(SSR)や静的生成(SSG)を実現するには、
getServerSideProps
やgetStaticProps
を明示的に定義する必要がある。 - クライアントに送信されるJavaScriptの量が多くなりがち。
App Router:
- デフォルトでサーバーコンポーネントとして動作し、サーバー側でレンダリングが完結。
- クライアントに送信されるのは結果のHTMLのみで、JavaScriptの負荷が大幅に軽減。
- クライアントサイドの機能が必要な場合は、
"use client"
を付けてクライアントコンポーネントに切り替え可能。これにより、サーバーとクライアントの役割を明確に分担できる。
例:
// app/products/[id]/page.js (サーバーコンポーネント)
export default async function ProductPage({ params }) {
const res = await fetch(`/api/products/${params.id}`);
const product = await res.json();
return <h1>{product.name}</h1>;
}
// app/counter/page.js (クライアントコンポーネント)
"use client";
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
- Pages Routerではクライアント側動作が前提だったが、App Routerでは用途に応じて柔軟に使い分けられる。
2. データフェッチングのシンプルさ
Pages Router:
- データフェッチには
getStaticProps
,getServerSideProps
,getInitialProps
など、特定の関数をエクスポートする必要がある。 - ページごとにデータ取得方法を分離して記述。
例:
// pages/products/[id].js
export async function getServerSideProps({ params }) {
const res = await fetch(`http://example.com/api/products/${params.id}`);
const product = await res.json();
return { props: { product } };
}
export default function ProductPage({ product }) {
return <h1>{product.name}</h1>;
}
App Router:
- データフェッチングをページやレイアウト内で直接記述でき、専用関数が不要。
- コードがシンプルになり、データとUIの関係が直感的。
例:
// app/products/[id]/page.js
export default async function ProductPage({ params }) {
const res = await fetch(`/api/products/${params.id}`);
const product = await res.json();
return <h1>{product.name}</h1>;
}
- 記述量が減り、可読性が向上。
3. 柔軟なレイアウト管理
Pages Router:
- レイアウトは
_app.js
やカスタムコンポーネントで一括管理。ページごとに異なるレイアウトを適用するには条件分岐や工夫が必要。
例:
// pages/_app.js
export default function MyApp({ Component, pageProps }) {
return (
<div>
<header>共通ヘッダー</header>
<Component {...pageProps} />
</div>
);
}
- ネストされたレイアウトは手動で実装。
App Router:
-
layout.js
で階層的なレイアウトを簡単に定義でき、フォルダごとに適用可能。 - ネストが自然に統合され、ページ遷移時の状態保持もサポート。
例:
// app/products/layout.js
export default function ProductsLayout({ children }) {
return (
<div>
<nav>商品ナビ</nav>
{children}
</div>
);
}
-
/products
以下のページに自動適用され、ルートレイアウト(app/layout.js
)と組み合わせ可能。
4. ローディングとエラー処理の統合
Pages Router:
- ローディング状態やエラー処理は自分で実装するか、
getServerSideProps
内で条件分岐。 - カスタム404ページは
_error.js
や404.js
で対応。
例:
export async function getServerSideProps({ params }) {
const res = await fetch(`/api/products/${params.id}`);
if (!res.ok) return { notFound: true };
return { props: { product: await res.json() } };
}
App Router:
-
loading.js
やerror.js
でローディングUIとエラーUIを簡単に定義でき、自動適用。 - エラー処理でインタラクティブな操作(例: リトライボタン)が必要な場合、
"use client"
を使ってクライアントコンポーネント化。 - 開発効率が向上し、ユーザー体験(UX)が一貫。
例:
// app/products/[id]/loading.js
export default function Loading() {
return <p>読み込み中...</p>;
}
// app/products/[id]/error.js
"use client";
export default function Error({ error, reset }) {
return <p>エラー: {error.message}</p>;
}
まとめ
- 概要: Next.js 13以降で導入された新ルーティングシステム。
-
基盤:
app
ディレクトリ内でファイルベースのルーティングを採用。 -
サーバーコンポーネント: デフォルトでサーバー側レンダリングを活用。クライアント動作が必要な場合は
"use client"
で切り替え。 -
データフェッチ: ページやレイアウト内で直接
fetch
を使用。 -
ルーティング:
-
page.js
でページを定義。 -
route.js
でAPIを定義。
-
-
レイアウト:
layout.js
で階層的なUI構築。 -
特別ファイル:
-
loading.js
: ローディングUI。 -
error.js
: エラーUI。 -
not-found.js
: 404ページ。
-
-
動的ルート:
[id]
や[...slug]
で柔軟に対応。 - 目的: パフォーマンス向上とモダンな開発体験の提供。
まとめると「サーバーファーストで直感的、かつ高機能な次世代ルーティング」と言えるでしょう。