Next.js13のappDir/layoutについての所感
はじめに
新年あけましておめでとうございます。
Next.js13では、従来のpages/
を使ったルーティングの他に、app/
を使ったルーティングが可能となりました。(pages/
も引き続きサポートされており、app/
は現在Beta扱いです)
appディレクトリは
- Nested Layout
- React Server Components
- Streaming
- Data Fetching
の4点について魅力的な機能が搭載されました。
ここでは、Nested Layoutについて、詰まったところや実装方法について述べます。
色々説明を端折っているので、この記事だけ読んでよし始めようとすると辛いかもですが、読んでから始めると混乱する場所をいくつか避けられるかもしれません。
create-next-app
Next.js13のプロジェクトは、
yarn create next-app --experimental-app
で作成可能です。
app/
が生成され、デフォルトで使用する設定になっていることがわかります。
const nextConfig = {
experimental: {
appDir: true,
},
}
また、pages/
もそのまま残っており、Next APIのファイルだけ残されています。
一応開発ロードマップにはAPIもapp/
に統合する計画はあるようで今後に期待です。
現在のところ、pages/api
下に記述して従来通り使用できます。
ルーティングは/api/*
にあたるみたいです。
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}
appDirのレンダリング
app/
ディレクトリ下にルートされたコンテンツは、基本的にデフォルトではSSRで処理されます。
クライアントに送信されるJavaScriptが減るので、高速化が見込めます。
また、よりネストの深いコンテンツをレイアウトファイルを書くことで一部だけレンダリングし直すことが可能となり、ヘッダ部を描き直さずにコンテンツだけ書き直すことができるようになりました。
したがって、共通と思われる場所は再レンダリングされません。
appDir下のファイル
通常app/
下には以下のファイルが作成できます。
- layout.tsx
- page.tsx
- template.tsx
- head.tsx
- *.css
head.tsxだけやや情報が少なかったのでメモしておきます。
head.tsx
head.tsxはhtmlのヘッダ情報を記述できるファイルです。ネストした小要素においてヘッダを書き換えたい時にも使用できるようです。
少し古いcreate-next-app
だとこのファイルを生成しない場合もありました。
export default function Head() {
return (
<>
<title>Create Next App</title>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</>
)
}
return (
<html lang="en">
{/*
<head /> will contain the components returned by the nearest parent
head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
*/}
<head />
<body>{children}</body>
</html>
)
URLのパスをネストしたくない時
route-groupsという機能が使用できます。
app/page.tsx
は/
にルートされ、app/about/page.tsx
は/about
にルートされるというのが
カッコで囲んだ子ディレクトリを作成すると、そのディレクトリはルーティングから無視され、ディレクトリの整理だけに使用することが可能となります。
例えば、
app/(user)/signin/page.tsx
とapp/(user)/signup/page.tsx
はそれぞれ
/signin
、/signup
にルートされます。
このとき、app/(user)/layout.tsx
を作成すれば、
app/(user)/signin/page.tsx
とapp/(user)/signup/page.tsx
双方に適用されます。
RootLayoutとNestingLayouts
簡単に考えると、「app/
に書くべき大元のレイアウトがRootLayoutで、app/about/
とかに書くべきなのがNestingLayouts」です。
そもそもRootLayoutとNestingLayoutsとはなんなのか
両方とも関数で書かれたもので、違いといえば、名前がRootLayoutかそうでないかです。
ドキュメントによると
The root layout is defined at the top level of the app directory and applies to all routes. This layout enables you to modify the initial HTML returned from the server.
とのことで、とりあえずapp/
直下に絶対必要です。
公式サンプルだと、ここにナビゲーションバーコンポーネントを読み込んでレイアウトして....
的なことをしてる例が載っていますが、ここでナビゲーションバーとかをレイアウトするのはお勧めできないです。(やらかした人) これについては後述します。
export default function RootLayout({ children }: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
export default function DashboardLayout({
children,
}: {
children: React.ReactNode,
}) {
return <section>{children}</section>;
}
page.tsxの置き場所について
pageの実体をつくるpage.tsx
ですが、layout.tsx
と異なり、app/
直下にある必要はありませんでした。というか、先入観で作りたくなっちゃうんですがそこには作らないほうがいいと思います。先ほどの問題と合わせて後述します。
page.tsx
は、app/
下のディレクトリ直下ではなく、ルーティングした後の/
下になる位置に必要です。
こちらの方の例をご覧ください。
app/layout.tsx
が存在し、ルーティングした後の/
にアクセスしたとき表示されるpage.tsx
は
app/(marketing)/page.tsx
という設計になっています。
appDirの仕様上、()で囲まれたディレクトリは無視されることを利用しています。
この状態でapp/(auth)/page.tsx
など作成してしまうとエラーとなります。
本題:実用的なappDir内ディレクトリ構成
以下のようなWebサイトを想定します。
1./
にアクセスすると、ナビゲーションバーのあるWebページが表示される
2./about
にアクセスすると、ナビゲーションバーはそのままでコンテンツが切り替わる
2./signin
にアクセスすると、ナビゲーションバーは消え、パスワード表示画面が全画面表示される。
これは失敗例です
app/
- layout.tsx // ここで nav.tsx をレイアウト
- page.tsx
- about/
| - layout.tsx
| - page.tsx
- signin/
| - layout.tsx
| - page.tsx
components/
- nav.tsx
最初私はこのようなディレクトリを構成しました。
直下のRootLayoutでナビゲーションバーをレイアウトし、必要なくなったところで上書きして消すイメージです。
import '../styles/globals.css'
import Nav from '../components/nav'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}): JSX.Element {
return (
<html lang='en'>
<body>
<Nav />
{children}
</body>
</html>
)
}
export default function DashboardLayout({
children,
}: {
children: React.ReactNode,
}) {
<html lang="en">
<body>
{children}
</body>
</html>
}
仕様1. 2.までは満たすことができましたが、3のナビゲーションバーを消すところで問題が発生しました
- ページを読み込むとき一瞬ナビゲーションバーが表示される。
- 戻るボタンで
/signin
から/
に戻るとナビゲーションバーが消える
いま思うと非常に愚かな話なのですがapp/
ディレクトリ下にルートされたコンテンツは、デフォルトではSSRで処理されますから、<body>
タグの直下まるごと書き換えるのは良くなかったようです。
解決策
app/
- layout.tsx
- (contents)
- layout.tsx // ここで nav.tsx をレイアウト
- page.tsx
- about/
| - layout.tsx
| - page.tsx
- (auth)
- layout.tsx
- signin/
| - page.tsx
components/
- nav.tsx
Next.js13を使う時は、最上位のRootLayoutは
<body>
{children}
</body>
くらい最小限にして、ナビゲーションバーを使うメインコンテンツは(contents)
などのルートした時/
直下になるディレクトリを作成して、ひとつlayout.tsxのネストを落として処理しましょう。
いまのところその予定がなくても、RootLayoutをシンプルにしてネストを落とすことでレンダリングに自由度が向上すると思われます。
/signin
にアクセスすると、(auth)
が無視されapp/(auth)/signin/page.tsx
がレンダリングされます。
この時、ナビゲーションバーをレイアウトしていたのはapp/(contents)/layout.tsx
ですから、route-groupsの有効範囲を超えてナビゲーションバーは正常に表示されなくなります。
終わりに
わかりにくい文章だったとは思いますが、いかがだったでしょうか。
このような現象が発生する詳しい原因等ご存知のかたいらっしゃいましたらアドバイスお願いします。
Discussion