Next.js13のappDir/layoutについての所感

2023/01/14に公開

はじめに

新年あけましておめでとうございます。

https://nextjs.org/blog/next-13

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/が生成され、デフォルトで使用する設定になっていることがわかります。

next.config.js
const nextConfig = {
  experimental: {
    appDir: true,
  },
}

また、pages/もそのまま残っており、Next APIのファイルだけ残されています。
https://nextjs.org/docs/api-routes/introduction

一応開発ロードマップにはAPIもapp/に統合する計画はあるようで今後に期待です。
現在のところ、pages/api下に記述して従来通り使用できます。
ルーティングは/api/*にあたるみたいです。

pages/api/hello.ts
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が減るので、高速化が見込めます。

また、よりネストの深いコンテンツをレイアウトファイルを書くことで一部だけレンダリングし直すことが可能となり、ヘッダ部を描き直さずにコンテンツだけ書き直すことができるようになりました。
したがって、共通と思われる場所は再レンダリングされません。

https://nextjs.org/blog/layouts-rfc

appDir下のファイル

https://beta.nextjs.org/docs/routing/pages-and-layouts

通常app/下には以下のファイルが作成できます。

  • layout.tsx
  • page.tsx
  • template.tsx
  • head.tsx
  • *.css

head.tsxだけやや情報が少なかったのでメモしておきます。

head.tsx

head.tsxはhtmlのヘッダ情報を記述できるファイルです。ネストした小要素においてヘッダを書き換えたい時にも使用できるようです。
少し古いcreate-next-appだとこのファイルを生成しない場合もありました。

head.tsx
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" />
    </>
  )
}
layout.tsx
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.tsxapp/(user)/signup/page.tsxはそれぞれ
/signin/signupにルートされます。
このとき、app/(user)/layout.tsxを作成すれば、
app/(user)/signin/page.tsxapp/(user)/signup/page.tsx双方に適用されます。

https://beta.nextjs.org/docs/routing/defining-routes#route-groups

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/直下に絶対必要です。
公式サンプルだと、ここにナビゲーションバーコンポーネントを読み込んでレイアウトして....
的なことをしてる例が載っていますが、ここでナビゲーションバーとかをレイアウトするのはお勧めできないです。(やらかした人) これについては後述します

app/layout.tsx
export default function RootLayout({ children }: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  return <section>{children}</section>;
}

page.tsxの置き場所について

pageの実体をつくるpage.tsxですが、layout.tsxと異なり、app/直下にある必要はありませんでした。というか、先入観で作りたくなっちゃうんですがそこには作らないほうがいいと思います。先ほどの問題と合わせて後述します。

page.tsxは、app/下のディレクトリ直下ではなく、ルーティングした後の/下になる位置に必要です。

こちらの方の例をご覧ください。
https://github.com/shadcn/taxonomy/tree/main/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でナビゲーションバーをレイアウトし、必要なくなったところで上書きして消すイメージです。

app/layout.tsx
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>
  )
}
app/signin/layout.tsx
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