Next13 の app ディレクトリ配下では、以下の特殊ファイルを定義することで、構成をより簡潔にし、ボイラープレートを減らすようにしています。
page.jslayout.jstemplate.jsloading.jshead.jserror.jsnot-found.jsroute.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 名 | 説明 | 例 |
|---|---|---|
| children | layout にラップされているページ 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.tsxはlayout.tsxと類似しており、ページをラップするのですが、違いは状態を共有しているページ間を移動した時に、子要素のインスタンスを作成し、再レンダリングされるという点です。
例えば、template 内で、アニメーションを行いたい場合や、useEffectやuseStateなどを用いてページ遷移ごとにログを取得する時などに必要になります。
パフォーマンスの観点からは、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>