UIを構成する特殊ファイル [page・layout・template]

Posted: 2023/2/24
Next.js/
Next13

Next13 の app ディレクトリ配下では、以下の特殊ファイルを定義することで、構成をより簡潔にし、ボイラープレートを減らすようにしています。

  • page.js
  • layout.js
  • template.js
  • loading.js
  • head.js
  • error.js
  • not-found.js
  • route.js

かなり数が多いので、今回は主に UI に関わる特殊ファイルについて説明させていただきます。

.js.jsx、または.tsxファイル拡張子を使用できます。

page.js(tsx)

Next13 の URL パスの決まり方と重複しますが、page.tsxはパスに紐づく UI を表し、default export したモジュールがページ UI となります。

props(page.js)

受け取る props は以下です。

props 名説明
params (optional)動的なフォルダ変数と動的なパスパラメータのオブジェクト{"id":["food"]}
searchParams (optional)クエリパラメータと値のオブジェクト{"q":"rice"}

どちらも URL に紐づくパラメータを受け取ることがわかります。

例(page.js)

では以下のようなディレクトリ構成・page ファイルの場合の挙動を確認してみましょう。

app
└── shops
    ├── [...id]
    │   └── page.tsx

  • page.tsx
import { NextPage } from "next";

interface PageProps {
  params: {
    id: string[];
  };
  searchParams?: { [key: string]: string | string[] | undefined };
}

const ShopPage: NextPage<PageProps> = ({ params, searchParams }) => {
  return (
    <main>
      <div>params: {JSON.stringify(params)}</div>
      <div>searchParams: {JSON.stringify(searchParams)}</div>
    </main>
  );
};

export default ShopPage;

この場合、/shops/food?q=riceにアクセスがあった場合、以下のような表示になります。

params: {"id":["food"]}
searchParams: {"q":"rice"}

layout.js(tsx)

layout.tsxは、定義されたディレクトリ以下のページ間で共有される UI です。layoutが共有されているページ間を遷移した場合、再レンダリングは発生しません。

props(layout.js)

以下の props を受け取ります。layout.tsxはページ UI をラップするので、必ずchildrenを受け取り、ラップして返す必要があります。

props 名説明
childrenlayout にラップされているページ UI(page.js, loading.js などが該当)
params (optional)動的なフォルダ変数と動的なパスパラメータのオブジェクト動的なフォルダ変数と動的なパスパラメータのオブジェクト

例(layout.js)

では実際のlayout.tsxの例を見てみます。以下の構成で、

app
└── shops
    ├── [id]
    │   ├── layout.tsx
    │   └── page.tsx
        └────── page.tsx

layout.tsxを以下のように定義します。

const ShopLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <h1>Shop</h1>
      {children}
    </div>
  );
};

export default ShopLayout;

この状態で、/shops/foodにアクセスすると、layout の状態が引き継がれ、Shop という文字が表示されるはずです、/shop/hogeにしても同様です。

しかし、この layout から外れている/shop にアクセスした場合は、layout の状態は引き継がれず、あくまでlayout.tsxと同階層以下の時に適用されることがわかります。

layout のユースケース

layoutには、ルートレイアウトページレイアウトと 2 種類の使い方があり、ページレイアウトは先ほど例に挙げたものになります。

一方でルートレイアウトとは、app ディレクトリ直下に置かれるlayout.tsxのことであり、アプリケーション全体のレイアウトを意味します。

app
├── layout.tsx
├── page.tsx

ルートレイアウト

ルートレイアウトは、今までの_document.tsx_app.tsxに代わるものであり、サーバーから返される最初の HTML を変更することができます。

  • app/layout.tsx
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <head />
      <body>{children}</body>
    </html>
  );
}

上の例のように、グローバルの CSS を読み込んだり、全体の HTML を定義しています。

ルートレイアウトに関しては、以下のような点に注意する必要があります。

  • ルートレイアウトは必須です
  • Next.js はタグを自動生成しないので、ここで<html>, <body>タグを定義する必要があります。
  • また、ルートレイアウトは Server Component になっており、Client Component に変更することができません。

template.js(tsx)

template.tsxlayout.tsxと類似しており、ページをラップするのですが、違いは状態を共有しているページ間を移動した時に、子要素のインスタンスを作成し、再レンダリングされるという点です。

例えば、template 内で、アニメーションを行いたい場合や、useEffectuseStateなどを用いてページ遷移ごとにログを取得する時などに必要になります。

パフォーマンスの観点からは、layout に劣るため、できれば layout を用いて、それで実現できない場合に利用するのがよいでしょう。

また、layout.tsx と同階層に置いた場合は、layout にラップされます(template が子要素になる)。

例(template.js)

以下のような構成とします。

app
└── shops
    ├── [id]
    │   ├── layout.tsx
    │   ├── page.tsx
    │   └── template.tsx

template.tsx, layout.tsxを以下のように定義します。

  • template.tsx
const ShopLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <h1>Shop Layout</h1>
      {children}
    </div>
  );
};

export default ShopLayout;
  • layout.tsx
const ShopLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <h1>Shop Layout</h1>
      {children}
    </div>
  );
};

export default ShopLayout;

この場合は、出力は以下のようになり、layout が template をラップしていることがわかるかと思います。

<div>
  <h1>Shop Layout</h1>
  <div>
    <h2>Shop Template</h2>
    <main>
      <!-- children -->
    </main>
  </div>
</div>