🐼

Next.js の App Router で 親layout を継承しない方法

2023/07/12に公開

Leaner Technologies の @corocn です。
最近は Next.js with App Router に入門しており、layout に関する学びを共有します。

本記事は Next.js v13.4.4 で検証しました。

子のpageで親のlayoutを継承したくない場合にどうするか?

まず App Router には Nested Layouts という機能があり、上層レイアウトの children で定義した部分に下層のレイアウトまたはページが描画され、入れ子のように描画されます。

https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#nesting-layouts

下層のレイアウト定義がシンプルになる一方、上層のレイアウトを無理やり修正することはできませんが、無視したいケースも一定存在します。そこで利用するのが Route Groups です。

Route Groups を利用する方法

Route Groups は "(auth)" のような括弧付きの記法です。Route Groups は URLのパスに影響を与えることなく、ディレクトリ・ファイルを論理的に分割することができます。

https://nextjs.org/docs/app/building-your-application/routing/route-groups

Route Groups を上手く利用することで、URLの構造を保ったままディレクトリを整理することができ、layout の適用範囲をコントロールすることができます。

Route Groups の例

例えば、次のようなルーティングを考えます。

path 内容
/ ログイン後のページ
/login ログインページ

これを愚直に定義すると次のようなディレクトリ構成になります。

/app
  layout.tsx (root layout with サイドバー付き)
  page.tsx (ログイン後のページ)
  /login
    page.tsx(サイドバーが表示されてしまう!)

サイドバーを描画したルートレイアウトを継承しているため、ログインページにもサイドバーが表示されてしまいます。

ここで Route Groups を利用すると、次のように整理することができます。

/app
  layout.tsx (root layout)
  (auth)
    /login
      layout.tsx(ログインページ用のレイアウト)
      page.tsx
  (authenticated)
    layout.tsx (サイドバーを描画したログイン後のレイアウト)
    page.tsx

layout はあくまで親ディレクトリを参照するため、ログインページにサイドバーが描画されることがなくなりました。

(auth) と (authenticated) は 実際のURLに反映されることはありません。あくまでディレクトリを整理するためだけに利用されます。

(ボツ) usePathname で分岐をしてみる

ログイン、非ログインのようにrootの近い場合の出し分けは Route Groups を利用すればよいのですが、ネストが深すぎてどうしようもないときもありそうです。

usePathname を使って無理やりレンダリングを制御する方法を試してみました。

例えば、次のようなケースを考えてみます。

/app
  layout.tsx (root layout)
  (website)
    layout.tsx
    page.tsx(website用レイアウト)
    /about
      /company
        page.tsx
        /form
          page.tsx(ここだけwebsite用レイアウトを使いたくない)

form 配下の page.tsx の描画で website用のレイアウトを継承したくないとします。

condition.tsx
"use client"
import { usePathname } from "next/navigation";

export default function Condition(props) {
  const pathname = usePathname();

  const isPlainLayout = pathname === "/about/company/form";

  const renderDefault = () => (
    <div>
      Default Layout
      { props.children }
    </div>
  )

  const renderPlain = () => (
    <div>
      Plain Layout
      { props.children }
    </div>
  )

  return isPlainLayout ? renderPlain() : renderDefault()
}
layout.tsx
import Condition from "@/app/(website)/condition";

export default function WebsiteLayout({children}) {
  return (
    <div>
      <Condition> { children } </Condition>
    </div>
  )
}

これで一応分岐できますが、ちょっと無理矢理感が否めません。

usePathname は Client Component only な hook のため、Client Component の中で Server Component を利用することになります。

今回は children に渡す形で利用しているので描画自体は成功していますが、最適化など、どこか落とし穴があるかもしれません。

Route Groups を使って分岐をしてみる

前述の例を Route Groups を使って分岐してみました。

/app
  layout.tsx (root layout)
  (website)
    layout.tsx
    page.tsx(website用レイアウト)
    /about
      /company
        page.tsx
  (plain)
    layout.tsx
    /about
      /company
        /form
	  /page.tsx

ディレクトリ構成が煩雑になるかな?と思いましたが、上層で分割されているため、意外と見やすいですね。いずれの場合も Route Groups を使うのが良さそうです。

まとめ

  • レイアウトを継承したくない場合は Route Groups を利用してディレクトリ構成を見直すこと
  • Route Groups は初見で「何この気持ち悪い記法」と思ったが、非常に便利

参考

採用

してます!

https://careers.leaner.co.jp/

リーナーテックブログ

Discussion