🤔

App Routerの基本的な仕組みとファイル構造

はじめに

App Routerは、Next.jsのバージョン13.4で安定版として登場したルーティングシステムです。
React Server Components(RSC)を基盤としており、サーバーサイドでの処理を可能にすることで、従来のPages Routerよりもパフォーマンスの向上を図っています。

App Routerの誕生背景

ReactとNext.jsの進化を振り返ると、App Routerの登場がいかに画期的かがわかります。

ReactとNext.jsの進化

Reactは2013年にFacebookによって公開され、コンポーネントベースの設計や仮想DOMなど、多くの革新的な概念を導入しました。
2016年には、Vercelの創設者であるGuillermo RauchによってNext.jsが登場。
Next.jsは、Reactの強力な機能を活かしつつ、サーバーサイドレンダリング(SSR)やファイルシステムベースのルーティングなど、Reactだけでは実現が難しかった機能を簡単に実装できるようにしました。
https://en.wikipedia.org/wiki/Next.js
https://vercel.com/about

従来のPage Routerとの違い

Next.jsのバージョン13以前では、Pages Router というルーティングシステムの使用が主流でした。
これは、pagesディレクトリ内のファイル構造に基づいてルーティングを自動的に生成するという、当時としては革新的なアプローチでした。

しかし、Pages Routerでは、主にクライアントサイドレンダリング(CSR)を採用しているため、コンポーネントが増えるとブラウザがダウンロードするJavaScriptの量が増え、初回ロード時に時間がかかるという問題がありました。

App Routerは、これらの問題を解決するために導入されました。

App Routerは、React Server Components(RSC)を基盤としており、サーバーサイドでの処理を可能にすることで、ブラウザにダウンロードするJavaScriptの量を削減し、パフォーマンスの向上を実現しています。

ファイルベースのルーティングシステム

基本定義

従来のPages Router では、/pages ディレクトリ配下のファイル構造がURLの構造になりました。

├── _app.tsx                   # グローバルレイアウトを定義する
├── index.tsx                  #  / にアクセスした際に表示する内容を定義する
└── signin.tsx                 # /signin にアクセスした際に表示する内容を定義する

同様の構成を App Router で作りましょう。

App Router では、ディレクトリ構造がそのままURLの構造になるので、直感的にルーティングの設定をすることができます。
ルーティングは/app配下のディレクトリベースで行います。

以下の構成であれば、「/」にアクセスするとapp/page.tsxが表示され、「/products」にアクセスするとapp/products/page.tsxが表示されるようになります。

   app
    └──  (Auth)                # ルートグループ。URLには影響しないが、関連するページをグループ化する
        ├── layout.tsx         # 共通化したいレイアウトを定義する
        └──  signin
             └── page.tsx      # /signinにアクセスした際に表示する内容を定義する

page.tsxには、そのページが読み込まれたときに最初に表示される内容を書きます。
下の例では、app/productsというURLにアクセスした際に表示される内容を定義しています。

app/products/page.tsx
export default function ProductsPage() {
  return (
    <div>
      <h1>Products</h1>
      <p>商品1</p>
      <p>商品2</p>
      <p>商品</p>
    </div>
  );
}

Pages RouterとApp Routerのファイル構造比較

アーキテクチャの思想がクライアントベースからサーバーサイドベースに変わったことで、Pages RouterとApp Routerとでは、ファイル構造も変化しました。

簡単に、従来のPages RouterとApp Routerの比較をしてみましょう。

Pages Router

pages/
├── _app.tsx                   # グローバルレイアウト (Client Component)
├── index.tsx                  # ルート: / (Client Component)
├── about.tsx                  # ルート: /about (Client Component)
├── products/
│   ├── index.tsx              # ルート: /products (Client Component)
│   └── [id].tsx               # ルート: /products/[id] (Client Component)
└── api/
    └── products.tsx           # API route: /api/products

components/
├── Header.tsx                 # 共通ヘッダー (Client Component)
├── Footer.tsx                 # 共通フッター (Client Component)
├── ProductList.tsx            # 製品リスト (Client Component)
└── ProductFilter.tsx          # 製品フィルター (Client Component)

App Router

app/
├── layout.tsx               # ルートレイアウト (Server Component)
├── page.tsx                 # ルート: / (Server Component)
├── about/
│   └── page.tsx             # ルート: /about (Server Component)
├── products/
│   ├── page.tsx             # ルート: /products (Server Component)
│   ├── [id]/
│   │   └── page.tsx         # ルート: /products/[id] (Server Component)
│   └── client-components/
│       ├── ProductList.tsx  # 製品リスト (Client Component)
│       └── ProductFilter.tsx # 製品フィルター (Client Component)
└── api/
    └── products/
        └── route.tsx        # API Route: /api/products

components/
├── Header.tsx               # 共通ヘッダー (Server Component)
└── Footer.tsx               # 共通フッター (Server Component)

Pages Router と App Router の主な違いは以下の通りです。
文章だけだとみづらかったので、表にまとめてみました。

Dynamic RoutesとRoute Groups

Dynamic Routes

Dynamic Routesは、角括弧[]を使って定義します。
例えば、app/products/[id]/page.tsxというファイルを作ると、/products/1、/products/2などのURLにマッチするページが生成されます。

app/
└── products/
    └── [id]/
        └── page.tsx
app/products/[id]/page.tsx
export default function ProductPage({ params }: { params: { category: string, id: string } }) {
  return (
    <>
      <h1>Category: {params.category}</h1>
      <h2>Product ID: {params.id}</h2>
    </>
  )
}

Route Groups

Next.jsは、ディレクトリ構造がそのままURLの構造になるのが便利な反面、場合によってはそれを避けたいこともあります。
そんなときに役立つのがRoute Groupsです。

Route Groups はディレクトリ名を()で括ることで作成できます。
()で囲ったディレクトリは、URLには影響しませんが、関連するページをグループ化するのに役立ちます。

例えば、app/(auth)/login/page.tsxというファイルを作っても、URLは/loginのままです。

app/
├── (auth)/
│   ├── login/
│   │   └── page.tsx
│   └── register/
│       └── page.tsx

また、関連するページ間のRoute Groupsを作成することで、グループ内のページ間でレイアウトを共有できるので、一貫したUIにすることが簡単にできますね。

ファイル規約

App Routerでは、ファイルの名前によって、役割が決まっています。
覚えておくと便利なので、目を通しておくと良いかと思います。

  • default.tsx:
    デフォルトの画面
  • error.tsx:
    404エラー画面
  • layout.tsx:
    共通のUIを設定する
  • loading.tsx:
    ローディング画面
  • middleware.tsx:
    リクエストが完了する前に実行されるコードを定義
  • not-found.tsx:
    notFound関数がスローされたときに表示する画面
  • page.tsx:
    ルートの画面
  • route.tsx:
    APIエンドポイントを定義
  • template.tsx:
    共通のUI。layout.tsxとは異なり、状態を保持せず、毎回再レンダリングされる

https://nextjs.org/docs/app/api-reference/file-conventions

ルート間のナビゲーション

Next.jsでは、ルート間のナビゲーションを行う主な方法が3つあります。

<Link>コンポーネント

最も推奨されるのは**<Link>コンポーネント**の使用です。

メリットとしては、ページを事前に読み込むプリフェッチング機能で、ユーザーがクリックする前に次のページの準備をするので、ページ遷移が速くなります。
また、クライアントサイドナビゲーションで、ページ全体をリロードせずにスムーズな遷移を実現できます。

<Link> を使うには、next/link からインポートし、hrefプロパティをコンポーネントに渡します。

app/page.tsx
import Link from 'next/link'

export default function Page() {
  return <Link href="/products">Products</Link>
}

useRouterフック

useRouterフックは、Next.jsのClient Components内でルートをプログラムで変更するための機能です。useRouterを使うことでと、フォーム送信後の遷移や、特定の条件が満たされた時の自動遷移など、様々なシチュエーションで活用できます。

ユーザーの操作に応じて動的にページ遷移をさせることができます。

app/page.tsx
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
  const router = useRouter()
  return (
    <button type="button" onClick={() => router.push('/products')}>
      Dashboard
    </button>
  )
}

この例では、ボタンをクリックすると /products ページに移動します。

ただし、注意点として、Server Componentsでは useRouter() は使用できません。
サーバーサイドでのリダイレクトが必要な場合は、代わりに redirect() 関数を使用します。
redirect() 関数は主に、認証チェックやデータの存在確認などのサーバーサイドのロジック後にリダイレクトする場合に使用されます。

③ ネイティブ History API

ネイティブHistory APIを使うと、ページをリロードせずにブラウザの履歴を操作できます。
主に2つのメソッドがあります。

1. window.history.pushState
新しい履歴エントリを追加するので、ブラウザの「戻る」ボタンを押すと前の状態に戻れます。
使用例: 商品リストのソート順を変更する際

2. window.history.replaceState
現在の履歴エントリを置き換えるので、ブラウザの「戻る」ボタンを押しても前の状態に戻れません。
使用例: アプリの言語設定を変更する際

これらのメソッドを使うと、URLを変更しつつ、ページ全体をリロードせずに済みます。
また、Next.jsのルーターと連携しているので、usePathnameuseSearchParamsなどのフックと一緒に使用でき、より細かい制御をすることができます。

https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating

ミドルウェアの活用と設定

App Routerはミドルウェアを使用することで、リクエストとレスポンスの間に追加の処理を挟むことができます。

App Router におけるミドルウェアとは?

ミドルウェアというのは、リクエストがサーバーに届いてから、実際にページコンポーネントが処理を始める前に、何かしらの追加処理を行いたい場合に使うものです。
言い換えると、リクエストとレスポンスの間に挟まって、何かしらの操作をするための「中間の層」といった感じでしょうか。

具体的に何ができるのか?

リクエストがサーバーに到達する前に実行されるので、例えば、ユーザーが特定のページにアクセスしたときに、ログインしているかどうかを確認したり、特定の条件に応じて別のURLにリダイレクトしたりする、といったことができます。

設定方法について

ミドルウェアを設定するには、プロジェクトのルートディレクトリ(またはsrcディレクトリ)にmiddleware.ts(または.js)ファイルを作成し、このファイル内でmiddleware関数をエクスポートします。

また、必要に応じて、matcher設定を使用して、ミドルウェアを適用するパスを指定します。

import { NextRequest, NextResponse } from 'next/server';

export const config = {
  matcher: ['/:path*', '/index/:path*'],
};

export function middleware(req: NextRequest) {
    // ミドルウェアのロジックをここに記述
}

実行順序について

ミドルウェアは、プロジェクト内のすべてのルートで動作します。
リクエスト処理の流れを順を追ってみましょう。

  1. まず、next.config.jsファイルで設定されたheadersが適用されます。
    これにより、HTTP応答ヘッダーをカスタマイズできます。
  2. 次に、同じくnext.config.jsで定義されたredirects(リダイレクト)ルールが評価されます。
  3. その後、ミドルウェアが実行されます。
    ここではrewrites(URLの書き換え)やredirectsなどの処理が可能です。
  4. next.config.jsのrewritesセクションにあるbeforeFilesオプションが処理されます。
    これは、ファイルシステムのルーティングよりも優先されます。
  5. 続いて、実際のファイルシステムに基づくルーティングが行われます。
    これにはpublic/ディレクトリ、_next/static/pages/app/ディレクトリなどが含まれます。
  6. ファイルシステムのルーティングの後、next.config.jsのrewritesセクションにあるafterFilesオプションが処理されます。
  7. 動的ルート(例:/blog/[slug]のような形式)の解決が行われます。
  8. 最後に、next.config.jsのrewritesセクションにあるfallbackオプションが処理されます。
    これは他のすべてのルールにマッチしなかった場合のフォールバックとして機能します。

https://nextjs.org/docs/app/building-your-application/routing/middleware#matching-paths

まとめ

今回はApp Routerの基本的な仕組み、ファイル構造、ルートグループ、そして重要なミドルウェアについて解説しました。
この記事で、App Routerの概要と基本をしっかりと押さえられたのではないかと思いますが、ここからが本番です!
次の記事では、App Routerで効率的なデータフェッチングについて詳しくみていく予定です!

最後までお読みいただきありがとうございました。

Discussion