Next.js App Routerの強力なファイルベースルーティングを、この詳細ガイドで解き明かしましょう。アプリケーションの構造化、動的ルートの作成、レイアウトの処理方法などを学べます。
Next.js App Router: ファイルベースルーティングの完全ガイド
Next.js 13で導入され、その後のバージョンで標準となったNext.js App Routerは、アプリケーションの構造化とナビゲーションの方法に革命をもたらします。これは、開発を簡素化し、パフォーマンスを向上させ、全体的な開発者体験を強化する、強力で直感的なファイルベースルーティングシステムを導入します。この包括的なガイドでは、App Routerのファイルベースルーティングを深く掘り下げ、堅牢でスケーラブルなNext.jsアプリケーションを構築するための知識とスキルを提供します。
ファイルベースルーティングとは?
ファイルベースルーティングは、アプリケーションのルート構造がファイルとディレクトリの構成によって直接決定されるルーティングシステムです。Next.js App Routerでは、`app`ディレクトリ内にファイルを作成することでルートを定義します。各フォルダはルートセグメントを表し、それらのフォルダ内の特別なファイルがそのルートセグメントの処理方法を定義します。このアプローチにはいくつかの利点があります:
- 直感的な構造: ファイルシステムがアプリケーションのルート構造を反映するため、理解しやすく、ナビゲートしやすいです。
- 自動ルーティング: Next.jsはファイル構造に基づいてルートを自動的に生成するため、手動での設定は不要です。
- コードのコロケーション: ルートハンドラとUIコンポーネントが一緒に配置されるため、コードの整理と保守性が向上します。
- 組み込み機能: App Routerはレイアウト、動的ルート、データフェッチなどの組み込みサポートを提供し、複雑なルーティングシナリオを簡素化します。
App Routerを始める
App Routerを使用するには、新しいNext.jsプロジェクトを作成するか、既存のプロジェクトを移行する必要があります。Next.jsバージョン13以降を使用していることを確認してください。
新しいプロジェクトの作成:
次のコマンドを使用して、App Routerを備えた新しいNext.jsプロジェクトを作成できます:
npx create-next-app@latest my-app --example with-app
既存プロジェクトの移行:
既存のプロジェクトを移行するには、`pages`ディレクトリから`app`ディレクトリにページを移動する必要があります。それに応じてルーティングロジックを調整する必要があるかもしれません。Next.jsは、このプロセスを支援するための移行ガイドを提供しています。
ファイルベースルーティングのコアコンセプト
App Routerは、ルートの処理方法を定義するいくつかの特別なファイルと規約を導入しています:
1. `app` ディレクトリ
`app`ディレクトリは、アプリケーションのルートの起点です。このディレクトリ内のすべてのファイルとフォルダがルートの生成に使用されます。`app`ディレクトリの外にあるもの(移行中の場合は`pages`ディレクトリなど)は、App Routerによって無視されます。
2. `page.js` ファイル
`page.js`(または`page.jsx`、`page.ts`、`page.tsx`)ファイルは、App Routerの最も基本的な部分です。特定のルートセグメントに対してレンダリングされるUIコンポーネントを定義します。これは、直接アクセス可能にしたいルートセグメントにとって必須のファイルです。
例:
このようなファイル構造があるとします:
app/
about/
page.js
ユーザーが`/about`にナビゲートすると、`app/about/page.js`からエクスポートされたコンポーネントがレンダリングされます。
// app/about/page.js
import React from 'react';
export default function AboutPage() {
return (
<div>
<h1>私たちについて</h1>
<p>私たちの会社について詳しくはこちら。</p>
</div>
);
}
3. `layout.js` ファイル
`layout.js`(または`layout.jsx`、`layout.ts`、`layout.tsx`)ファイルは、ルートセグメント内の複数のページで共有されるUIを定義します。レイアウトは、一貫したヘッダー、フッター、サイドバーなど、複数のページに存在するべき要素を作成するのに便利です。
例:
`/about`ページと、仮の`/about/team`ページの両方にヘッダーを追加したいとします。`app/about`ディレクトリに`layout.js`ファイルを作成できます:
// app/about/layout.js
import React from 'react';
export default function AboutLayout({ children }) {
return (
<div>
<header>
<h1>私たちの会社について</h1>
</header>
<main>{children}</main>
</div>
);
}
`children` propは、同じディレクトリまたはネストされたディレクトリの`page.js`ファイルによってレンダリングされるUIに置き換えられます。
4. `template.js` ファイル
`template.js`ファイルは`layout.js`に似ていますが、各子ルートに対してコンポーネントの新しいインスタンスを作成します。これは、子ルート間をナビゲートする際にコンポーネントの状態を維持したり、再レンダリングを防ぎたい場合に便利です。レイアウトとは異なり、テンプレートはナビゲーション時に再レンダリングされます。 テンプレートの使用は、ナビゲーション時の要素のアニメーションに最適です。
例:
// app/template.js
'use client'
import { useState } from 'react'
export default function Template({ children }) {
const [count, setCount] = useState(0)
return (
<main>
<p>Template: {count}</p>
<button onClick={() => setCount(count + 1)}>テンプレートを更新</button>
{children}
</main>
)
}
5. `loading.js` ファイル
`loading.js`(または`loading.jsx`、`loading.ts`、`loading.tsx`)ファイルを使用すると、ルートセグメントの読み込み中に表示されるローディングUIを作成できます。これは、データフェッチやその他の非同期操作を実行する際に、より良いユーザー体験を提供するのに役立ちます。
例:
// app/about/loading.js
import React from 'react';
export default function Loading() {
return <p>About情報を読み込み中...</p>;
}
ユーザーが`/about`にナビゲートすると、`page.js`コンポーネントが完全にレンダリングされるまで`Loading`コンポーネントが表示されます。
6. `error.js` ファイル
`error.js`(または`error.jsx`、`error.ts`、`error.tsx`)ファイルを使用すると、ルートセグメント内でエラーが発生したときに表示されるカスタムエラーUIを作成できます。これは、よりユーザーフレンドリーなエラーメッセージを提供し、アプリケーション全体がクラッシュするのを防ぐのに役立ちます。
例:
// app/about/error.js
'use client'
import React from 'react';
export default function Error({ error, reset }) {
return (
<div>
<h2>エラーが発生しました!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>再試行</button>
</div>
);
}
`/about`ページのレンダリング中にエラーが発生した場合、`Error`コンポーネントが表示されます。`error` propにはエラーに関する情報が含まれ、`reset`関数によってユーザーはページの再読み込みを試みることができます。
7. ルートグループ
ルートグループ `(groupName)` を使用すると、URL構造に影響を与えることなくルートを整理できます。フォルダ名を括弧で囲むことによって作成されます。これは、レイアウトや共有コンポーネントを整理するのに特に役立ちます。
例:
app/
(marketing)/
about/
page.js
contact/
page.js
(shop)/
products/
page.js
この例では、`about`ページと`contact`ページは`marketing`グループに、`products`ページは`shop`グループに属しています。URLはそれぞれ`/about`、`/contact`、`/products`のままです。
8. 動的ルート
動的ルートを使用すると、可変セグメントを持つルートを作成できます。これは、データベースやAPIからフェッチしたデータに基づいてコンテンツを表示するのに便利です。動的ルートセグメントは、セグメント名を角括弧で囲むことによって定義されます(例: `[id]`)。
例:
IDに基づいて個々のブログ投稿を表示するルートを作成したいとします。次のようなファイル構造を作成できます:
app/
blog/
[id]/
page.js
`[id]`セグメントは動的セグメントです。`app/blog/[id]/page.js`からエクスポートされたコンポーネントは、ユーザーが`/blog/123`や`/blog/456`のようなURLにナビゲートしたときにレンダリングされます。`id`パラメータの値は、コンポーネントの`params` propで利用可能になります。
// app/blog/[id]/page.js
import React from 'react';
export default async function BlogPost({ params }) {
const { id } = params;
// 与えられたIDでブログ投稿のデータをフェッチ
const post = await fetchBlogPost(id);
if (!post) {
return <p>ブログ投稿が見つかりません。</p>;
}
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
async function fetchBlogPost(id) {
// データベースやAPIからのデータフェッチをシミュレート
return new Promise((resolve) => {
setTimeout(() => {
const posts = {
'123': { title: '最初のブログ投稿', content: 'これは最初のブログ投稿の内容です。' },
'456': { title: '別のブログ投稿', content: 'これはさらに興味深い内容です。' },
};
resolve(posts[id] || null);
}, 500);
});
}
ルートに複数の動的セグメントを使用することもできます。たとえば、`/blog/[category]/[id]`のようなルートを持つことができます。
9. キャッチオールセグメント
キャッチオールセグメントを使用すると、任意の数のセグメントに一致するルートを作成できます。これは、URL構造がユーザーによって決定されるCMSのようなシナリオで便利です。キャッチオールセグメントは、セグメント名の前に3つのドットを追加することによって定義されます(例: `[...slug]`)。
例:
app/
docs/
[...slug]/
page.js
`[...slug]`セグメントは、`/docs`の後の任意の数のセグメントに一致します。たとえば、`/docs/getting-started`、`/docs/api/users`、`/docs/advanced/configuration`に一致します。`slug`パラメータの値は、一致したセグメントを含む配列になります。
// app/docs/[...slug]/page.js
import React from 'react';
export default function DocsPage({ params }) {
const { slug } = params;
return (
<div>
<h1>ドキュメント</h1>
<p>Slug: {slug ? slug.join('/') : 'No slug'}</p>
</div>
);
}
オプションのキャッチオールセグメントは、セグメント名を二重角括弧 `[[...slug]]` で囲むことで作成できます。これにより、ルートセグメントがオプションになります。 例:
app/
blog/
[[...slug]]/
page.js
この設定では、`/blog` と `/blog/any/number/of/segments` の両方で page.js コンポーネントがレンダリングされます。
10. パラレルルート
パラレルルートを使用すると、同じレイアウトで1つ以上のページを同時にレンダリングできます。これは、ページの異なるセクションを独立してロードできるダッシュボードなどの複雑なレイアウトに特に便利です。パラレルルートは、`@`記号に続いてスロット名(例: `@sidebar`、`@main`)を使用して定義されます。
例:
app/
@sidebar/
page.js // サイドバー用のコンテンツ
@main/
page.js // メインセクション用のコンテンツ
default.js // 必須: パラレルルートのデフォルトレイアウトを定義
`default.js`ファイルは、パラレルルートを使用する際に必須です。これは、異なるスロットがどのように組み合わされて最終的なレイアウトを作成するかを定義します。
// app/default.js
export default function RootLayout({ children: { sidebar, main } }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: '200px', backgroundColor: '#f0f0f0' }}>
{sidebar}
</aside>
<main style={{ flex: 1, padding: '20px' }}>
{main}
</main>
</div>
);
}
11. インターセプトルート
インターセプトルートを使用すると、アプリケーションの別の部分からのルートを現在のレイアウト内で読み込むことができます。これは、モーダル、画像ギャラリーなど、既存のページコンテンツの上に表示されるべきUI要素を作成するのに便利です。インターセプトルートは、`(..)`構文を使用して定義されます。これは、インターセプトするルートを見つけるためにディレクトリツリーをどれだけ上にさかのぼるかを示します。
例:
app/
(.)photos/
[id]/
page.js // インターセプトされたルート
feed/
page.js // 写真モーダルが表示されるページ
この例では、ユーザーが`/feed`ページで写真をクリックすると、`app/(.)photos/[id]/page.js`ルートがインターセプトされ、`/feed`ページの上にモーダルとして表示されます。`(.)`構文は、Next.jsに`photos/[id]`ルートを見つけるために1レベル上(`app`ディレクトリ)を見るように指示します。
App Routerでのデータフェッチ
App Routerは、サーバーコンポーネントとクライアントコンポーネントを使用したデータフェッチの組み込みサポートを提供します。サーバーコンポーネントはサーバーでレンダリングされ、クライアントコンポーネントはクライアントでレンダリングされます。これにより、各コンポーネントの要件に基づいて最適なアプローチを選択できます。
サーバーコンポーネント
サーバーコンポーネントは、App Routerのデフォルトです。これにより、別のAPIルートを必要とせずに、コンポーネント内で直接データをフェッチできます。これにより、パフォーマンスが向上し、コードが簡素化されます。
例:
// app/products/page.js
import React from 'react';
export default async function ProductsPage() {
const products = await fetchProducts();
return (
<div>
<h1>製品一覧</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
async function fetchProducts() {
// データベースやAPIからのデータフェッチをシミュレート
return new Promise((resolve) => {
setTimeout(() => {
const products = [
{ id: 1, name: '製品A' },
{ id: 2, name: '製品B' },
{ id: 3, name: '製品C' },
];
resolve(products);
}, 500);
});
}
この例では、`fetchProducts`関数が`ProductsPage`コンポーネント内で直接呼び出されます。コンポーネントはサーバーでレンダリングされ、データはHTMLがクライアントに送信される前にフェッチされます。
クライアントコンポーネント
クライアントコンポーネントはクライアントでレンダリングされ、イベントリスナー、状態、ブラウザAPIなどのクライアントサイド機能を使用できます。クライアントコンポーネントを使用するには、ファイルの先頭に`'use client'`ディレクティブを追加する必要があります。
例:
// app/counter/page.js
'use client'
import React, { useState } from 'react';
export default function CounterPage() {
const [count, setCount] = useState(0);
return (
<div>
<h1>カウンター</h1>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>インクリメント</button>
</div>
);
}
この例では、`CounterPage`コンポーネントは`useState`フックを使用しているため、クライアントコンポーネントです。`'use client'`ディレクティブは、Next.jsにこのコンポーネントをクライアントでレンダリングするように指示します。
高度なルーティングテクニック
App Routerは、複雑で洗練されたアプリケーションを作成するために使用できるいくつかの高度なルーティングテクニックを提供します。
1. ルートハンドラ
ルートハンドラを使用すると、`app`ディレクトリ内にAPIエンドポイントを作成できます。これにより、別の`pages/api`ディレクトリが不要になります。ルートハンドラは`route.js`(または`route.ts`)という名前のファイルで定義され、異なるHTTPメソッド(例: `GET`、`POST`、`PUT`、`DELETE`)を処理する関数をエクスポートします。
例:
// app/api/users/route.js
import { NextResponse } from 'next/server'
export async function GET(request) {
// データベースからユーザーをフェッチするシミュレーション
const users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' },
];
return NextResponse.json(users);
}
export async function POST(request) {
const body = await request.json()
console.log('受信データ:', body)
return NextResponse.json({ message: 'ユーザーが作成されました' }, { status: 201 })
}
この例では、`/api/users`に`GET`と`POST`の両方のリクエストを処理するルートハンドラを定義しています。`GET`関数はユーザーのリストを返し、`POST`関数は新しいユーザーを作成します。
2. 複数のレイアウトを持つルートグループ
ルートグループとレイアウトを組み合わせて、アプリケーションの異なるセクションに異なるレイアウトを作成できます。これは、サイトの異なる部分に異なるヘッダーやサイドバーを持たせたい場合に便利です。
例:
app/
(marketing)/
layout.js // マーケティング用レイアウト
about/
page.js
contact/
page.js
(admin)/
layout.js // 管理画面用レイアウト
dashboard/
page.js
この例では、`about`ページと`contact`ページは`marketing`レイアウトを使用し、`dashboard`ページは`admin`レイアウトを使用します。
3. ミドルウェア
ミドルウェアを使用すると、リクエストがアプリケーションによって処理される前にコードを実行できます。これは、認証、認可、ロギング、ユーザーの場所やデバイスに基づくリダイレクトなどのタスクに便利です。
ミドルウェアは、プロジェクトのルートにある`middleware.js`(または`middleware.ts`)という名前のファイルで定義されます。
例:
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// ユーザーが認証済みかチェック
const isAuthenticated = false; // 認証ロジックに置き換えてください
if (!isAuthenticated && request.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// 詳細は以下の「Matching Paths」を参照
export const config = {
matcher: '/admin/:path*',
}
この例では、ユーザーが`/admin`以下のルートにアクセスする前に認証済みかどうかをチェックするミドルウェアを定義しています。ユーザーが認証されていない場合、`/login`ページにリダイレクトされます。
ファイルベースルーティングのベストプラクティス
App Routerのファイルベースルーティングシステムを最大限に活用するために、次のベストプラクティスを検討してください:
- ファイル構造を整理する: 意味のあるフォルダ名を使用し、関連するファイルをまとめます。
- 共有UIにはレイアウトを使用する: ヘッダー、フッター、サイドバーなど、複数のページで共有される要素にはレイアウトを作成します。
- ローディングUIを使用する: データフェッチやその他の非同期操作を行うルートには、ローディングUIを提供します。
- エラーを適切に処理する: エラー発生時に優れたユーザー体験を提供するために、カスタムエラーUIを作成します。
- 整理のためにルートグループを使用する: URL構造に影響を与えずにルートを整理するためにルートグループを使用します。
- パフォーマンス向上のためにサーバーコンポーネントを活用する: サーバーコンポーネントを使用してサーバーでデータをフェッチし、UIをレンダリングすることで、パフォーマンスとSEOを向上させます。
- 必要な場合はクライアントコンポーネントを使用する: イベントリスナー、状態、ブラウザAPIなどのクライアントサイド機能が必要な場合は、クライアントコンポーネントを使用します。
Next.js App Routerでの国際化の例
Next.js App Routerは、ファイルベースルーティングを通じて国際化(i18n)を簡素化します。以下に、i18nを効果的に実装する方法を示します:
1. サブパスルーティング
サブパスを使用して、ロケールに基づいてルートを整理します。例:
app/
[locale]/
page.tsx // ロケールごとのホームページ
about/
page.tsx // ロケールごとのAboutページ
// app/[locale]/page.tsx
import { getTranslations } from './dictionaries';
export default async function HomePage({ params: { locale } }) {
const t = await getTranslations(locale);
return (<h1>{t.home.title}</h1>);
}
// dictionaries.js
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
ja: () => import('./dictionaries/ja.json').then((module) => module.default),
};
export const getTranslations = async (locale) => {
try {
return dictionaries[locale]() ?? dictionaries.en();
} catch (error) {
console.error(`ロケール ${locale} の翻訳読み込みに失敗しました`, error);
return dictionaries.en();
}
};
この設定では、`[locale]`動的ルートセグメントが異なるロケール(例: `/en`、`/ja`)を処理します。翻訳はロケールに基づいて動的に読み込まれます。
2. ドメインルーティング
より高度なアプローチとして、各ロケールに異なるドメインまたはサブドメインを使用できます。これには通常、ホスティングプロバイダーでの追加設定が必要です。
3. ロケール検出のためのミドルウェア
ミドルウェアを使用して、ユーザーの優先ロケールを自動検出し、適切にリダイレクトします。
// middleware.js
import { NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
let locales = ['en', 'ja', 'fr'];
function getLocale(request) {
const negotiatorHeaders = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
let languages = new Negotiator({ headers: negotiatorHeaders }).languages();
try {
return match(languages, locales, 'ja'); // デフォルトロケールとして "ja" を使用
} catch (error) {
console.error("ロケールのマッチングエラー:", error);
return 'ja'; // マッチング失敗時のフォールバックとして日本語を使用
}
}
export function middleware(request) {
const pathname = request.nextUrl.pathname;
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
return NextResponse.redirect(
new URL(
`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
request.url
)
);
}
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
このミドルウェアは、リクエストされたパスにロケールプレフィックスがあるかどうかを確認します。ない場合、`Accept-Language`ヘッダーを使用してユーザーの優先ロケールを検出し、適切なロケール固有のパスにリダイレクトします。`@formatjs/intl-localematcher`や`negotiator`のようなライブラリがロケールネゴシエーションの処理に使用されます。
Next.js App Routerとグローバルアクセシビリティ
グローバルにアクセス可能なWebアプリケーションを作成するには、アクセシビリティ(a11y)の原則を慎重に考慮する必要があります。Next.js App Routerは、アクセシブルな体験を構築するための強固な基盤を提供しますが、能力に関係なく誰もがアプリケーションを使用できるように、ベストプラクティスを実装することが不可欠です。
主要なアクセシビリティの考慮事項
- セマンティックHTML: コンテンツを構造化するために、セマンティックなHTML要素(例: `<article>`, `<nav>`, `<aside>`, `<main>`)を使用します。これにより、支援技術に意味が提供され、ユーザーがサイトをより簡単にナビゲートできるようになります。
- ARIA属性: カスタムコンポーネントやウィジェットのアクセシビリティを向上させるために、ARIA(Accessible Rich Internet Applications)属性を使用します。ARIA属性は、要素の役割、状態、プロパティに関する追加情報を支援技術に提供します。
- キーボードナビゲーション: すべてのインタラクティブな要素がキーボードでアクセス可能であることを確認します。ユーザーは`Tab`キーを使用してアプリケーションをナビゲートし、`Enter`または`Space`キーを使用して要素と対話できる必要があります。
- 色のコントラスト: 視覚障害のあるユーザーの読みやすさを確保するために、テキストと背景の間に十分な色のコントラストを使用します。Web Content Accessibility Guidelines(WCAG)は、通常のテキストには少なくとも4.5:1、大きなテキストには3:1のコントラスト比を推奨しています。
- 画像のaltテキスト: すべての画像に説明的なaltテキストを提供します。altテキストは、スクリーンリーダーによって読み上げられる画像のテキスト代替を提供します。
- フォームのラベル: `<label>`要素を使用して、フォームのラベルを対応する入力フィールドに関連付けます。これにより、各フィールドにどのような情報が期待されているかがユーザーに明確になります。
- スクリーンリーダーテスト: 視覚障害のあるユーザーがアクセス可能であることを確認するために、スクリーンリーダーでアプリケーションをテストします。人気のスクリーンリーダーには、NVDA、JAWS、VoiceOverなどがあります。
Next.js App Routerでのアクセシビリティの実装
- Next.js Linkコンポーネントを使用する: ナビゲーションには`<Link>`コンポーネントを使用します。プリフェッチやフォーカス管理など、組み込みのアクセシビリティ機能を提供します。
- フォーカス管理: ページ間を移動したり、モーダルを開いたりする際には、フォーカスが適切に管理されていることを確認します。フォーカスは、新しいページやモーダルの最も論理的な要素に設定されるべきです。
- アクセシブルなカスタムコンポーネント: カスタムコンポーネントを作成する際には、上記の原則に従ってアクセシブルであることを確認します。セマンティックHTML、ARIA属性、キーボードナビゲーションを使用して、コンポーネントを誰もが使用できるようにします。
- リンティングとテスト: ESLintなどのリンティングツールをアクセシビリティプラグインと共に使用して、コード内の潜在的なアクセシビリティの問題を特定します。また、自動テストツールを使用して、アプリケーションのアクセシビリティ違反をテストします。
結論
Next.js App Routerのファイルベースルーティングシステムは、アプリケーションを構造化し、ナビゲートするための強力で直感的な方法を提供します。このガイドで概説したコアコンセプトとベストプラクティスを理解することで、堅牢でスケーラブル、そして保守性の高いNext.jsアプリケーションを構築できます。App Routerのさまざまな機能を試してみて、それがどのように開発ワークフローを簡素化し、ユーザー体験を向上させることができるかを発見してください。