はじめに
Next.js13で登場した新しいルーティングの機能「App Router」についてまとめる😊
この記事で伝えたいこと
- App Routerとは何か
- 基本的な考え方
- 新しい機能
結論
✅App Routerは1フォルダ = 1ページのルーティング。
✅フォルダの中にpage.jsを作るとページができる。
✅ルーティング以外にも追加機能、廃止機能がある。
App Routerを学ぶ際の心構え
説明を読む前にApp Routerを「どんなメンタルで学ぶといいか」個人的に感じたことをお伝えする😊
学ぶ前の勝手なイメージ
App Routerという名前を聞いて
- ルーティングの仕方が変わったのかな〜💭
- フォルダ構成がちょっと変わるくらいかな〜💭
と軽く見ていた…💦
学び始めてからのギャップ
しかし変更点はルーティングの方法だけではなかった😢
追加機能、廃止機能がありそれなりに学習コストがかかる💦
最初からそのつもりで学習するとよさそう!
【補足】公式ドキュメントも2個になった
App Router用のページがちょっと増えたとかではなく、App Router用のドキュメントが丸々1個増えた。
それくらいApp Routerは大規模💦
左上でApp RouterとPages Routerのドキュメントを切り替えられる。
【前提】App Routerって何?
App Routerの仕組みを理解する前に、まずは全体像を理解する。
App Routerとは
✅新しいルーティングのこと。
1フォルダ = 1ページのルーティングになった!
ルーティング以外の変更点もある
他にも追加機能や廃止機能がある。
後で解説するので、一旦は「ルーティング以外にも変わった部分があるのね💭」くらいでOK!
App Routerの導入方法
✅Next.js13以降は何もしなくてもApp Routerが使える。
【補足】Next.js12以前からアップデートする場合
以下の2段階でApp Routerに移行できる。
1️⃣App Routerに以降する前に、Next.js13で書き方が変わった部分を対応する。
2️⃣App Routerに移行する。
(Pages RouterとApp Routerは併用できるので1ページずつゆっくり移行してもOK!)
App Routerのルーティング
Pages Routerとの比較
フォルダ構成がそのままURLになるのは変わらない😊
主な変更点をざっくり比較する。
Pages Router | App Router |
---|---|
1ファイル = 1ページ | 1フォルダ = 1ページ |
ファイル名がURLになる | フォルダ名がURLになる |
ファイル名は何でもOK | ファイル名が決まっている |
pagesフォルダを使う | appフォルダを使う |
App Routerでページを作る方法
「フォルダ」と「page.js」で1ページ作れる😊
例:2ページだけのフォルダ構成
🚫Pages Routerの場合
pages/
├── index.js --> /
└── about.js --> /about
✅App Routerの場合
app/
├── page.js --> /
└── about/
└── page.js --> /about
→ページの数だけpage.jsが必要。
特別な意味を持つファイルが登場した
appフォルダ内では「page.js」など特別な意味を持つファイルがある。
ファイル名の早見表
各フォルダの中で以下のファイルは特別な意味を持つ。
app/
├── page.js --> ページの中身。これが一番よく使うファイル⭐️
├── route.js --> APIの定義(page.jsと共存不可)
├── layout.js --> 共通の見た目
├── loading.js --> 読み込み中の画面
├── error.js --> エラー時の画面
├── global-error.js --> グローバルエラー画面
├── templete.js --> 共通の見た目
├── default.js --> デフォルトの画面
└── not-found.js --> notFound関数がスローされたときの画面
page.js
✅ページの中身を書くところ。ページを作るとき必須。
(逆に言うと、APIを作るときは不要。)
app/
└── page.js --> /(トップページ)
例:「/about」ページ
「aboutフォルダ」と「page.js」を作るだけでOK😊
フォルダ構成
app/
└── about/
└── page.js --> /about
app/about/page.js
// Aboutと表示するだけのページ
const Page = () => {
return <div>About</div>;
};
export default Page;
URL「/about」にアクセスする。
route.js
✅APIの定義を書くところ。APIエンドポイントを作るときに必須。
(逆に言うと、ページを作るときは不要。)
例1:GETメソッド(ユーザーリストを取得するAPI)
route.jsにGETメソッドを書くだけでOK😊
フォルダ構成
app/
└── users/
└── route.js --> /users でアクセスできるAPIエンドポイント
app/users/route.js
import { NextResponse } from "next/server";
// ユーザーリストを取得するAPI
export function GET() {
return NextResponse.json([
{
id: 1,
name: "山田 太郎",
},
{
id: 2,
name: "佐藤 次郎",
},
]);
}
URL「/users」にアクセスする。
例2:POSTメソッド(form送信された値を保存するAPI)
route.jsにPOSTメソッドを書くだけでOK😊
フォルダ構成
app/
└── users/
└── route.js --> /users でアクセスできるAPIエンドポイント
app/users/route.js
import { NextResponse } from "next/server";
// form送信された値を保存するAPI
export async function POST(request) {
// 送信されたデータを受け取る
const res = await request.json();
// DBに保存する処理など
// ...
}
【動作確認用】POST送信するページ
app/user-register/page.js
"use client";
import React, { useState } from "react";
export default function Register() {
const [username, setUsername] = useState("");
const handleSubmit = async (event) => {
event.preventDefault();
try {
const response = await fetch("/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({name: username}),
});
if (!response.ok) {
throw new Error("Response is not OK");
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
};
return (
<form onSubmit={handleSubmit} className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 text-3xl">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="username">
ユーザー名:
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline mb-4"
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit"
>
送信
</button>
</form>
);
}
【補足】requestを受け取る
GETメソッドに引数をつけるだけでリクエスト情報(NextRequest型のデータ)を受け取ることができる。
例:リクエスト情報からクエリパラメータを受け取る
/app/users/route.js
import { NextResponse } from "next/server";
export function GET(request) {
// リクエスト情報からクエリパラメータを取得
const { searchParams } = new URL(request.url);
// クエリパラメータ「name」の値を取得
const name = searchParams.get("name");
// 「http://localhost:3000/users?name=佐藤」のとき「佐藤」が表示される
console.log(name);
// 省略
}
layout.js
✅共通の見た目、処理、metaタグを書く。
「layout.jsがあるフォルダ以下」すべてに共通の見た目が適用される。
例1:全ページで共通のレイアウト
「/app/layout.js」は全ページ共通の見た目を定義できる😊
主にmetaタグ、<html>や<body>といった全体のレイアウトを定義するのに使われる。
フォルダ構成
app/
├── page.js
└── layout.js --> 全ページで共通の見た目
app/layout.js
// 共通のCSS
import { Inter } from 'next/font/google'. // (*1)
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
// 共通のmetaタグ
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
// 共通の見た目
export default function RootLayout({ children }) { // (*2)
return (
<html lang="en">
<body className={`${inter.className} text-red-500`}>{children}</body>
</html>
)
}
URL「/」や「/about」にアクセスする。
→全ページでlayout.jsに書いたCSSが適用される。
細かいコードの解説
(*1) import { Inter } from 'next/font/google'
✅Next.jsのフォント最適化の機能。
詳細は公式ドキュメントを参照。
(*2) children
✅childrenの部分にpage.jsが埋め込まれる。
<body className={inter.className}>{children}</body>
例2:「/product」以下で共通のレイアウト
「/app/product/layout.js」を作るだけで/product以下のページで共通のレイアウトを定義できる😊
フォルダ構成
app/
├── page.js
└── products/
├── page.js
├── layout.js --> 「/products」以下のページで共通の見た目
└── ...
app/products/layout.jsx
// products以下のページは、sectionタグで囲む
export default function ProductLayout({ children }) {
return <section className="任意のCSS">{children}</section>
}
→URL「/products」や「/products/abc」にアクセスするとlayout.jsが適用される。
【補足】下位の階層にもlayout.jsがある場合はどうなる?
✅結論、両方のlayout.jsが適用される。
例
app/
├── page.js --> /
├── layout.js --> 1️⃣「/」以下で共通の見た目
└── about/
├── page.js --> /about
└── layout.js --> 2️⃣「/about」以下で共通の見た目
1️⃣app/layout.tsx
→「/」以下で共通の見た目
→<html>や<body>といった全体のレイアウトを定義するのに使われる。
import './globals.css'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
2️⃣app/about/layout.tsx
→「/about」以下で共通の見た目
→「/about/〇〇」ページ特有のスタイルの定義に使われる。
export default function AboutLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex justify-center items-center h-screen">{children}</div>
);
}
{children}
に埋め込まれる。最終的に出力されるHTML
→両方のlayout.tsxが適用される。
<html lang="en">
<body className={inter.className}>
<div className="flex justify-center items-center h-screen">page.jsの中身</div>
</body>
</html>
【補足】下位のページでmetaタグを上書きする方法
✅各ページのpage.jsでmetaタグを上書きできる。
app/
├── page.js --> /
├── layout.js --> 全ページで共通の見た目
└── about/
└── page.js --> /about (ここでmetaタグを上書きしたい)
静的なメタデータの場合:metadataに指定するだけでOK😊
/app/about/page.js
// metaタグを上書き
export const metadata = {
title: 'Aboutページ',
description: 'Aboutページの説明文',
}
const Page = () => {
return <div>About</div>;
};
export default Page;
動的なメタデータの場合:generateMetadata関数の戻り値でmetaタグが設定できる😊
/app/about/page.js
export async function generateMetadata({params}) {
// 仮想のデータ取得処理
const user = await getUser(params.id);
// メタデータを返す
return { title: user.name };
}
const Page = async ({ params }) => {
// 省略
};
export default Page;
loading.js
✅サーバーでレンダリングしている間に表示される画面。
例:最小限のローディング画面
loading.jsを作るだけでローディング画面が作れる😊
フォルダ構成
app/
├── page.js --> /
└── loading.js --> 読み込み中の画面
app/loading.js
// ローディング中に表示する画面
export default function Loading() {
return (
<div className="flex justify-center items-center h-screen font-bold">
ローディング中
</div>
);
}
【動作確認用】レンダリングに時間がかかるpage.js
3秒待って、ユーザー一覧データを取得するページ。
app/page.js
const Page = async () => {
// 3秒待機
await new Promise((resolve) => setTimeout(resolve, 3000));
// ユーザ一覧を取得(前述のroute.jsで作ったAPIエンドポイントを叩く)
const response = await fetch('http://localhost:3000/users');
const users = await response.json();
console.log(users);
return (
<div className="m-4">
<h1 className="text-lg font-bold">ユーザ一覧</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
export default Page;
URL「/」にアクセスする。
→ローディング画面が表示される。
error.js
✅エラーが発生したときに表示される画面。
例:最小限のエラー画面
error.jsを作るだけでエラー時の画面が作れる😊
フォルダ構成
app/
├── page.js --> /
└── error.js. --> エラー時の画面
app/error.js
'use client';
// エラー時に表示する画面
const Error = ({ error }) => {
return (
<div>
<p>{error.message}</p>
</div>
);
}
export default Error;
【動作確認用】エラーを投げるpage.js
データの取得に失敗するページ。
app/page.js
const Page = async () => {
// ユーザ一覧を取得(存在しないURLを指定しているため失敗する)
const response = await fetch('http://localhost:3000/hoge');
// レスポンスがエラーの場合はエラーを投げる
if (!response.ok) throw new Error("データの取得に失敗しました");
const users = await response.json();
console.log(users);
return (
<div>失敗するとここは表示されない。error.jsが表示される</div>
);
};
export default Page;
URL「/」にアクセスする。
→エラー画面が表示される。
【発展】再読み込みボタン
✅reset関数を実行するとコンポーネントを再読み込みできる。
app/error.js
"use client";
// ✅引数にresetを付ける
const Error = ({ error, reset }) => {
return (
<div>
<p>{error.message}</p>
// ✅クリック時にreset関数を実行する
<button className="bg-blue-500 py-2 px-4 rounded mt-10" onClick={() => reset()}>
再読み込み
</button>
</div>
);
};
export default Error;
再読み込みボタンをクリックを作った場合の画面イメージ
特別な意味を持つフォルダが登場した
appフォルダ内では[ ]
で囲んだフォルダなど特別な意味を持つフォルダがある。
フォルダ名の早見表
以下のフォルダ名は特別な意味を持つ。
[〇〇]
:動的パス
app/
└── [○○] --> 動的パス
└── page.js --> /abc, /xyz, /12345, ...
[〇〇]/[〇〇]
:動的パス(2階層)
app/
└── [○○] --> 2階層の動的パス
└── [○○] --> 2階層の動的パス
└── page.js --> /abc/123, /xyz/456 ...
[[ ]]
:動的パス(省略可)
app/
└── [[○○]] --> 動的パス(省略可)
└── page.js --> /, /abc, /xyz, /12345, ...
[...〇〇]
:複数階層の動的パス
app/
└── [...○○] --> 複数階層の動的パス(何階層でもOK)
└── page.js --> /abc, /abc/123 ...
(〇〇)
:URLには影響しないフォルダになる
app/
└── (○○) --> パスには影響しない
├── hoge
│ └── page.js --> /hoge
└── fuga
└── page.js --> /fuga
@〇〇
:直接アクセスできないページが作れる
app/
├── layout.js --> ここから「@任意の名前/page.js」が読み込める
└── @○○/
└── page.js --> 直接アクセスできないページ(読み込まれる用)
(.)〇〇
:直接アクセスしたときと、Linkで飛んだときで表示するページを分けられる
app/
├── (.)○○
│ └── page.js/ -->Linkで飛んだときに表示される
└── ○○
└── page.js/ -->直接アクセスしたとき表示される
_〇〇
:ルーティングに含まれないフォルダが作れる
app/
└── _○○ --> ルーティングに含まれない
└── ...
[〇〇]
:動的パス
✅フォルダ名をapp/
[○○]
/page.js
にするだけで動的なパスになる。
(考え方はPages Routerと同じ)
例:/products/〇〇
app/products/[id]/page.js
ファイルを作成するだけで動的パスが実現できる😊
app/
└── products/
└── [id]
└── page.js --> /products/1, /products/2, ...
→/products/〇〇
にアクセスできる。
[〇〇]/[〇〇]
:動的パス(2階層)
✅フォルダ名をapp/
[○○]/[○○]
/page.js
にすると2階層に対応した動的なパスになる。
例:/products/〇〇/〇〇
app/products/[id]/[userid]/page.js
ファイルを作成するだけで2階層の動的パスが実現できる😊
app/
└── products/
└── [id]
└── [userid]
└── page.js --> /products/1/101, /products/1/102, ...
→/products/〇〇/〇〇
にアクセスできる。
[[ ]]
:動的パス(省略可)
✅フォルダ名をapp/
[[○○]]
/page.js
にするとパスの省略が可能になる。
例:/products/〇〇
app/products/[[id]]/page.js
ファイルを作成するだけで動的パスが実現できる😊
(末尾のパスを省略可能)
app/
└── products/
└── [[id]]
└── page.js --> /product, /products/1, /products/2, ...
→「/products/1」 や 「/products/2」 だけでなく「/products」にもアクセス可能。
[...〇〇]
:動的パス(複数階層)
✅フォルダ名をapp/
[...○○]
/page.js
にするだけで複数階層に対応した動的なパスになる。
→Cathc-all Routesという機能
例:/products/〇〇/〇〇/.../〇〇
app/products/[...id]/page.js
ファイルを作成するだけで何階層でもOKな動的パスが実現できる😊
app/
└── products/
└── [...id]
└── page.js --> /products/1, /products/2/abc, ...
→/products/〇〇/〇〇/.../〇〇
にアクセスできる。(何階層でもOK!)
idの部分は何でもOK。
(〇〇)
:URLには影響しないフォルダになる
✅フォルダ名をapp/
(○○)
にするとルーティングをグループ化できる。
→Route Groupsという機能。
これにより、URLは同じ階層のまま、異なるlayout.jsを適用できる。
Route Groupsがない場合の問題点
🚫通常はフォルダ名がURLに含まれてしまう。
app/
└── products/
├── layout.js --> products以下でこのレイアウトを適用したい
├── abc/
│ └── page.js --> /products/abc
└── xyz/
└── page.js --> /products/xyz
→当然URLにproductsが含まれてしまう。
/products
を含めたくない場合もある!Route Groupsによる解決
✅(○○)
とするだけでURLに含まないパターンを実現できる!
app/
├── page.js --> /
└── (products)/
├── layout.js --> products以下でこのレイアウトを適用したい
├── abc/
│ └── page.js --> /abc
└── xyz/
└── page.js --> /xyz
→URLにproductsが含まれない!
app/products/(product)
はグループ化するためだけのフォルダ。URLには影響しない。layout.js
の継承を区切るのに便利!
(.)〇〇
:直接アクセスしたときと、Linkで飛んだときで表示するページを分けられる
✅フォルダ名をapp/
(.)○○
にするとルーティングを横取りできる。
→Intercepting Routesという機能。
例:リンクをクリックしたときに横取りする
app/
(.)photo
を作るだけで、Linkで飛んだときのルーティングを横取りできる😊
フォルダ構成
app/
├── (.)photo
│ └── page.js/ --> /photo ※Linkで飛んだときに表示される(下記のpage.jsを表示せずに横取りする)
└── photo
└── page.js/ --> /photo ※直接アクセスしたときに表示される
■動作イメージ
直接ページを開いたときや、aタグで開いたときは通常どおりphoto
ページが表示される。
Linkタグで遷移した場合は(.)photo
のページが表示される。
(通常のphoto
ページが表示されずに横取りされる)
■動作まとめ
- 基本的に
(.)photo
とphoto
はセットで使う。 - 通常は
photo
のページが表示されるが、Linkで遷移したときは(.)photo
のページが表示される。
_〇〇
:ルーティングに含まれないフォルダが作れる
✅フォルダ名をapp/
_○○
にするとルーティングの対象外になる。
例:/components/〇〇
app/_components
フォルダはルーティング対象外になる😊
app/
└── _components/ --> ルーティング対象外
└── ...
→コンポーネントをまとめる用のフォルダが簡単に作れる。
App Routerによるルーティングの説明はここまで。
以降は、App Routerに関係する機能についてまとめる📝
App Routerで廃止・変更された機能
App Routerでは一部の機能の廃止され、書き方が変わる。
データの取得方法(SSR、SSGのやり方も変わる)
App Routerではfetch関数でデータを取得するようになった😊
🚫Pages Router
- データを取得するときはgetStaticProps関数、getServerSideProps関数を使っていた。
✅App Router
- データを取得するときはfetch関数を使う。
(getStaticProps関数、getServerSideProps関数が廃止された。)
useRouter
App Routerではインポートの仕方が変わった。
🚫Pages Router
- 'next/router'からインポートしていた。
- どのコンポーネントでも使用できた。
(そもそもサーバーコンポーネント、クライアントコンポーネントという概念がなかった)
import { useRouter } from 'next/router'
✅App Router
- 'next/navigation'からインポートするように変わった。
- クライアントコンポーネントでのみ使用可能になった。
'use client'
import { useRouter } from 'next/navigation'
静的HTMLの出力方法
HTMLを出力するときにnext export
が不要になった。
🚫Page Router
- 以下のコマンドを実行する。
next build & next export
✅App Router
- next.config.jsに
output: "export"
を追加する。 - 以下のコマンドを実行する。
next build
→
next export
は不要になった!
App Routerで追加された機能
App Routerではいくつか便利な機能が追加された。
サーバーコンポーネント
Reactのサーバーコンポーネントが使えるようになった!
(パフォーマンスの向上が期待できる)
🚫Pages Router
- クライアントコンポーネントしかなかった💦
✅App Router
- サーバーコンポーネントが使えるようになった✨
(デフォルトはサーバーコンポーネントだが、従来どおりクライアントコンポーネントも使える。)
Server Actions
クライアント側のコードの中に、サーバー側のコードを書けるようになった!
🚫Pages Router
- サーバー側のコードを書くにはAPIエンドポイントを作る必要があった💦
✅App Router
- クライアント側のコードに中に直接サーバー側のコードを書くことができるようになった✨
→Server Actions
- 従来どおりAPIエンドポイントを作ることもできる。
フック
新しいフックが複数追加された✨
- useSearchParams
URLのクエリ文字列を取得する
- usePathname
URLのパス名を取得する
- useParams
URLの動的パラメータを取得する
- useReportWebVitals
Core Web Vitalsのデータを取得する
- useSelectedLayoutSegments
フック実行場所以降のURLを取得する。
- useSelectedLayoutSegment
フック実行場所の1階層下のURLを取得する。
fetchしたデータを自動でキャッシュしてくれる
2回目以降のfetchはキャッシュされたデータが使われるようになった!
🚫Pages Router
- 同じfetchを何回もするとパフォーマンスが悪化してしまう💦
- fetchを1回で済ませるために、取得したデータを親→子にpropsで受け渡す。
✅App Router
- 同じfetchを何回してもパフォーマンスが悪化しない✨
- 好きなところで好きなだけfetchしていいので、取得したデータを親→子にpropsで受け渡す必要がない。
【補足】他にも全部で4種類のキャッシュがある
✅4種類キャッシュがあり、適切に使い分けることでパフォーマンスが向上する。
しかし開発者はあまり意識する必要がなく、デフォルトでいい感じに4種類を使わけてくれる✨
よくある疑問
App Routerのいいところは?
✅実際に使ってみて個人的にいいと感じたのは以下の2点。
ファイルを管理しやすい
1ページ = 1フォルダになったことで、ページごとにファイルをまとめやすくなった。
app/
├── page.js
├── about/
│ └── ... --> ✅aboutページで使うファイルはここにまとめる
└── products/
└── ... --> ✅productsページで使うファイルはここにまとめる
共通のレイアウトが作りやすい
layout.jsというファイルが使えるようなったことで、共通のレイアウトが作りやすくなった。
app/
├── page.js
├── layout.js --> ✅全ページで共通の見た目
└── products/
├── page.js
└── layout.js --> ✅products以下のページで共通の見た目
App Routerの悪いところは?
✅実際に使ってみて個人的に悪いと感じたのは以下の2点。
学習コストがかかる
これまでNext.jsを使っていた人でもそれなりに学習しないといけないのが大変。
Server Actionsの使い分けが難しい
バックエンドの処理が2パターンで書けるようになって、どっちで書けばいいか迷う。
- 従来どおりAPIエンドポイントを作る。
- Server Actionsでクライアント側のコードの中に、バックエンドのコードを書く。
Pages Routerとどっちを使えばいい?
✅公式ドキュメントではこれからはApp Routerが推奨されている!
まとめ
App Routerは全体的にPages Routerから大きく変わっている印象を受けた💦
それなりに学習コストもかかるが、今後はApp Routerが主流になるようなので少しずつ慣れていくのが大事そう😊