App Routerの落とし穴 二重サイドバーとルートグループ編 [Next.js]
今回解説する落とし穴は「二重サイドバー」
複数回に渡って、Next.jsのApp Routerの「落とし穴」を解説する。
前回 に引き続き、コンポーネント設計に関するミスを見ていこう。今回は レイアウトの継承ミス について、細かく解説していく。
問題が発生する状況
下記のようなブログを作るとする。
/ (トップページ)
└── articles (記事一覧ページ)
└── [articleId] (記事ページ)
トップページを作ろう。app/layout.tsx
にレイアウトを書いて...
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className="min-h-screen flex flex-col bg-gray-200">
<header className="p-3 text-center bg-yellow-200">
すごい!ホームページ
</header>
<div className="container mx-auto flex flex-col flex-grow md:flex-row bg-white">
<aside className="p-3 bg-blue-300">ここにサイドバー</aside>
<main className="p-3 flex-grow">{children}</main>
</div>
<footer className="p-3 bg-black text-white text-center">
フッター
</footer>
</body>
</html>
);
}
サイドバーを置けば共通化できるな。
export default async function Home() {
return <div>トップページ</div>;
}
app/page.tsx
はこれで大丈夫かな?
記事一覧ページを作ろう
src/app
+ ├── articles
+ │ ├── layout.tsx
+ │ └── page.tsx
├── layout.tsx
└── page.tsx
mkdir src/app/articles
touch src/app/articles/layout.tsx
touch src/app/articles/page.tsx
/articles
(記事一覧ページ)も作ってみよう。
export default function ArticleIndexLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex flex-col flex-grow md:flex-row">
<aside className="p-3 bg-blue-300">
<h2>記事一覧</h2>
</aside>
<main className="p-3 flex-grow bg-white">{children}</main>
</div>
);
}
export default function ArticleIndex() {
return <div>ここに記事一覧</div>;
}
ヨシ!これでいいかな?
あれ? サイドバーが2重になっちゃった!
何が間違っていたのか
説明用にレイアウトの範囲を示した。「レイアウトの継承」がうまくいってない 事がわかる。
src/app
├── articles
│ ├── layout.tsx ✅ ◀ 1. 記事一覧ページのレイアウト
│ └── page.tsx
├── layout.tsx ✅ ◀ 2. トップページのレイアウト (継承)
└── page.tsx
これは、App Routerのレイアウト継承順を表している。
記事一覧の親ディレクトリに layout.tsx
があるせいで、どちらも使われてしまうのだ。
上記の問題に遭遇し、「継承を諦めて、それぞれの page.tsx
にレイアウトを書く」ような人もいるかもしれない。だが、それじゃApp Routerを活かせていない!
ちゃんと解決策はあるから、落ち着いて聞いてほしい。
1. 何を実現したいのかリストアップする
今回達成したかった目的は、以下である。
- トップページ(/)と記事一覧がある
- トップページ(/)と記事一覧(/articles)で、ヘッダーとフッターは同じ
- トップページ(/)と記事一覧(/articles)で、サイドバーの中身を変える
2. トップページのlayout.tsxの記述は必要最小限にする
「ヘッダーとフッターは同じ」だが「サイドバーの中身を変える」必要がある。
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className="min-h-screen flex flex-col bg-gray-200">
<header className="p-3 text-center bg-yellow-200">
すごい!ホームページ
</header>
<div className="container mx-auto flex flex-col flex-grow md:flex-row bg-white">
- <aside className="p-3 bg-blue-300">ここにサイドバー</aside>
- <main className="p-3 flex-grow">{children}</main>
+ {children}
</div>
<footer className="p-3 bg-black text-white text-center">
フッター
</footer>
</body>
</html>
);
}
そこで、app/layout.tsx
から、ヘッダーとフッターは残しつつ、サイドバーを消す。
app/layout.tsx
には、サイト全体で絶対変わらないものだけを記述する。 このレイアウトから逃れることはできないからだ。
export default async function Home() {
- return <div>トップページ</div>;
+ return (
+ <>
+ <aside className="p-3 bg-blue-300">ここにサイドバー</aside>
+ <div>トップページ</div>
+ </>
+ );
}
app/page.tsx
に、トップページ用のサイドバーを移動するのを忘れずに。
3. 継承してほしくないレイアウトをルートグループで囲む
ちょっと待った! 一覧があるということは、「その下の階層」も考える必要がある。
例えば、/articles/[articleId]
(記事ページ)を作り、「サイドバーに目次が出る」zenn風レイアウトを追加したとする。
src/app
├── articles
+│ ├── [articleId]
+│ │ ├── layout.tsx ✅ ◀ 1. 記事ページのレイアウト
+│ │ └── page.tsx
│ ├── layout.tsx ✅ ◀ 2. 記事一覧ページのレイアウト (継承)
│ └── page.tsx
├── layout.tsx ✅ ◀ 3. トップページのレイアウト (継承)
└── page.tsx
このままでは、記事ページが「記事一覧」のレイアウトを継承し、また二重サイドバーになる!
目的に立ち返る
いよいよ詰んだか? いや、目的をはっきりさせよう。
- 記事ページに、目次を出したい
- ⇔ 記事ページが「記事一覧」のレイアウトを継承しない
- ⇔ 「記事一覧」のレイアウトが、記事ページの親でない
- ⇔ 「記事一覧」のレイアウトの階層が、下にズレている
- ⇔ 「記事一覧」のレイアウトが、記事ページの親でない
- ⇔ 記事ページが「記事一覧」のレイアウトを継承しない
階層をずらす? そんな事が可能なのか?
必殺!ルートグループ
そこで、記事一覧を「ルートグループ」で囲んでみよう。
「ルートグループ」 とは、半角カッコでディレクトリを作って、囲んだルート群のこと。
mkdir "src/app/articles/(list)"
mv src/app/articles/*.tsx "src/app/articles/(list)/"
一覧の articles/*.tsx
を articles/(list)/
に入れてみよう。
src/app
├── articles
+│ ├── (list) ⭐ ◀ これがルートグループ
+│ │ ├── layout.tsx
+│ │ └── page.tsx
│ └── [articleId]
│ ├── layout.tsx
│ └── page.tsx
-│ ├── layout.tsx
-│ └── page.tsx
├── layout.tsx
└── page.tsx
すると、こんな構成になる。正直気持ち悪いかもしれないが、App Routerにおけるパズルを解くために、必要な配置なのだ。 何が起こるかというと...
src/app
├── articles
│ ├── (list) ⭐
│ │ ├── layout.tsx ❌ ◀ 記事ページでは無視される
│ │ └── page.tsx
│ └── [articleId]
│ ├── layout.tsx ✅ ◀ 1. 記事ページのレイアウト
│ └── page.tsx
├── layout.tsx ✅ ◀ 2. トップページのレイアウト
└── page.tsx
グループで囲めば「親」ではなくなるため、継承順から無視される。
記事のページを開いても、一覧のサイドバーが混ざることはなくなった!
まとめ
App Routerで「二重サイドバー」に悩んだら、
- 何を実現したいのかリストアップする
- トップページのlayout.tsxの記述は必要最小限にする
- 継承してほしくないレイアウトをルートグループで囲む
の3つを実行しよう。
その他の落とし穴
Discussion