Backブログに戻る

Layouts RFC(リクエストフォーコメント)

ネストされたルートとレイアウト、クライアントとサーバーのルーティング、React 18の機能、そしてサーバーコンポーネント向けに設計されています。

このRFC(Request for Comment)は、Next.jsが2016年に導入されて以来の最大のアップデートを概説しています:

  • ネストされたレイアウト: ネストされたルートで複雑なアプリケーションを構築
  • サーバーコンポーネント向け設計: サブツリーナビゲーションに最適化
  • データ取得の改善: ウォーターフォールを避けつつレイアウトでデータ取得
  • React 18機能の活用: ストリーミング、トランジション、サスペンス
  • クライアントとサーバーのルーティング: SPAのような動作を持つサーバー中心のルーティング
  • 100%段階的採用可能: 破壊的変更なしで徐々に導入可能
  • 高度なルーティングパターン: 並列ルート、インターセプトルートなど

新しいNext.jsルーターは、最近リリースされたReact 18の機能を基盤として構築されます。これらの新機能を簡単に採用し、その利点を活かせるよう、デフォルトと規約を導入する予定です。

このRFCの作業は進行中であり、新機能が利用可能になった際には発表します。フィードバックを提供するには、Github Discussionsの会話に参加してください。

目次

動機

GitHub、Discord、Reddit、および開発者調査から、Next.jsの現在のルーティングの制限に関するコミュニティフィードバックを収集してきました。以下の点が明らかになりました:

  • レイアウト作成の開発者体験は改善の余地がある。ネスト可能で、ルート間で共有でき、ナビゲーション時に状態が保持されるレイアウトを簡単に作成できるべき。
  • 多くのNext.jsアプリケーションはダッシュボードやコンソールであり、より高度なルーティングソリューションの恩恵を受ける。

現在のルーティングシステムはNext.jsの開始以来うまく機能してきましたが、よりパフォーマンスが高く機能豊富なWebアプリケーションを開発者が簡単に構築できるようにしたいと考えています。

フレームワークのメンテナーとしても、後方互換性がありReactの将来と整合するルーティングシステムを構築したいと考えています。

注記: 一部のルーティング規約は、MetaのRelayベースのルーター(サーバーコンポーネントの機能が最初に開発された場所)や、React RouterやEmber.jsなどのクライアントサイドルーターからインスピレーションを得ています。layout.jsファイル規約はSvelteKitの作業に触発されました。Cassidyレイアウトに関する以前のRFCを開いてくれたことにも感謝します。

用語

このRFCでは、新しいルーティング規約と構文を導入します。用語はReactと標準的なWebプラットフォームの用語に基づいています。RFC全体で、これらの用語が以下の定義にリンクされます。

  • ツリー: 階層構造を視覚化するための規約。例えば、親と子コンポーネントを持つコンポーネントツリー、フォルダ構造など。
  • サブツリー: ツリーの一部で、ルート(最初)からリーフ(最後)まで。

  • URLパス: ドメインの後に続くURLの部分。
  • URLセグメント: スラッシュで区切られたURLパスの部分。

現在のルーティングの仕組み

現在、Next.jsはファイルシステムを使用して、Pagesディレクトリ内の個々のフォルダとファイルをURLでアクセス可能なルートにマッピングしています。各ページファイルはReactコンポーネントをエクスポートし、ファイル名に基づいて関連するルートを持ちます。例えば:

appディレクトリの紹介

これらの新しい改善を段階的に採用でき、破壊的変更を避けるために、appという新しいディレクトリを提案します。

appディレクトリはpagesディレクトリと並行して動作します。アプリケーションの一部を新しいappディレクトリに徐々に移動して、新機能の利点を活用できます。後方互換性のために、pagesディレクトリの動作は同じままで、引き続きサポートされます。

ルートの定義

app内のフォルダ階層を使用してルートを定義できます。ルートは、ルートフォルダから最終的なリーフフォルダまでの階層に従った、ネストされたフォルダの単一のパスです。

例えば、appディレクトリに2つの新しいフォルダをネストすることで、新しい/dashboard/settingsルートを追加できます。

注記:

  • このシステムでは、フォルダを使用してルートを定義し、ファイルを使用してUIを定義します(layout.jspage.js、およびRFCの後半でloading.jsなどの新しいファイル規約を使用)。
  • これにより、appディレクトリ内に独自のプロジェクトファイル(UIコンポーネント、テストファイル、ストーリーなど)を配置できます。現在、これはpageExtensions設定でのみ可能です。

ルートセグメント

サブツリー内の各フォルダはルートセグメントを表します。各ルートセグメントは、URLパスの対応するセグメントにマッピングされます。

例えば、/dashboard/settingsルートは3つのセグメントで構成されます:

  • /ルートセグメント
  • dashboardセグメント
  • settingsセグメント

注記: ルートセグメントという名前は、URLパスに関する既存の用語と一致するように選択されました。

レイアウト

新しいファイル規約: layout.js

これまで、アプリケーションのルートを定義するためにフォルダを使用してきました。しかし、空のフォルダはそれ自体では何もしません。新しいファイル規約を使用して、これらのルートにレンダリングされるUIを定義する方法について説明しましょう。

レイアウトは、サブツリー内のルートセグメント間で共有されるUIです。レイアウトはURLパスに影響を与えず、ユーザーが兄弟セグメント間をナビゲートしても再レンダリングされません(Reactの状態は保持されます)。

レイアウトは、layout.jsファイルからReactコンポーネントをデフォルトエクスポートすることで定義できます。コンポーネントは、レイアウトがラップしているセグメントで埋められるchildrenプロップを受け入れる必要があります。

レイアウトには2つのタイプがあります:

  • ルートレイアウト: すべてのルートに適用
  • 通常のレイアウト: 特定のルートに適用

2つ以上のレイアウトをネストしてネストされたレイアウトを形成できます。

ルートレイアウト

appフォルダ内にlayout.jsファイルを追加することで、アプリケーションのすべてのルートに適用されるルートレイアウトを作成できます。

注記:

  • ルートレイアウトは、カスタムApp(_app.jsカスタムDocument(_document.jsの必要性を置き換えます。なぜなら、すべてのルートに適用されるからです。
  • ルートレイアウトを使用して、初期ドキュメントシェル(例:<html>および<body>タグ)をカスタマイズできます。
  • ルートレイアウト(および他のレイアウト)内でデータを取得できます。

通常のレイアウト

特定のフォルダ内にlayout.jsファイルを追加することで、アプリケーションの一部にのみ適用されるレイアウトも作成できます。

例えば、dashboardフォルダ内にlayout.jsファイルを作成すると、dashboard内のルートセグメントにのみ適用されます。

ネストされたレイアウト

レイアウトはデフォルトでネストされます。

例えば、上記の2つのレイアウトを組み合わせるとします。ルートレイアウト(app/layout.js)はdashboardレイアウトに適用され、dashboard/*内のすべてのルートセグメントにも適用されます。

ページ

新しいファイル規約: page.js

ページは、ルートセグメントに固有のUIです。フォルダ内にpage.jsファイルを追加することでページを作成できます。

例えば、/dashboard/*ルートのページを作成するには、各フォルダ内にpage.jsファイルを追加できます。ユーザーが/dashboard/settingsにアクセスすると、Next.jsはsettingsフォルダのpage.jsファイルを、サブツリーの上位にあるレイアウトでラップしてレンダリングします。

dashboardフォルダ内に直接page.jsファイルを作成して、/dashboardルートにマッチさせることができます。ダッシュボードレイアウトもこのページに適用されます:

このルートは2つのセグメントで構成されます:

  • /ルートセグメント
  • dashboardセグメント

注記:

  • ルートが有効であるためには、リーフセグメントにページが必要です。ない場合、ルートはエラーをスローします。

レイアウトとページの動作

  • ファイル拡張子js|jsx|ts|tsxは、ページとレイアウトに使用できます。
  • ページコンポーネントはpage.jsのデフォルトエクスポートです。
  • レイアウトコンポーネントはlayout.jsのデフォルトエクスポートです。
  • レイアウトコンポーネントは必ずchildrenプロップを受け入れる必要があります。

レイアウトコンポーネントがレンダリングされると、childrenプロップは、子レイアウト(サブツリーのさらに下に存在する場合)またはページで埋められます。

親レイアウトが最も近い子レイアウトを選択し、ページに到達するまで続くレイアウトツリーとして視覚化するとわかりやすいかもしれません。

例:

app/layout.js
// ルートレイアウト
// - すべてのルートに適用
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}
app/dashboard/layout.js
// 通常のレイアウト
// - app/dashboard/*内のルートセグメントに適用
export default function DashboardLayout({ children }) {
  return (
    <>
      <DashboardSidebar />
      {children}
    </>
  );
}
app/dashboard/analytics/page.js
// ページコンポーネント
// - `app/dashboard/analytics`セグメントのUI
// - `acme.com/dashboard/analytics`URLパスにマッチ
export default function AnalyticsPage() {
  return <main>...</main>;
}

上記のレイアウトとページの組み合わせは、以下のコンポーネント階層をレンダリングします:

コンポーネント階層
<RootLayout>
  <Header />
  <DashboardLayout>
    <DashboardSidebar />
    <AnalyticsPage>
      <main>...</main>
    </AnalyticsPage>
  </DashboardLayout>
  <Footer />
</RootLayout>

Reactサーバーコンポーネント

注記: Reactは新しいコンポーネントタイプを導入しました:サーバー、クライアント(従来のReactコンポーネント)、および共有。これらの新しいタイプについて詳しく知るには、ReactのサーバーコンポーネントRFCを読むことをお勧めします。

このRFCでは、Reactの機能を使用し始め、ReactサーバーコンポーネントをNext.jsアプリケーションに段階的に採用できます。

新しいルーティングシステムの内部も、ストリーミング、サスペンス、トランジションなどの最近リリースされたReact機能を活用します。これらはReactサーバーコンポーネントの基礎となるビルディングブロックです。

サーバーコンポーネントがデフォルト

pagesappディレクトリの間の最大の変更の1つは、デフォルトで**app内のファイルがReactサーバーコンポーネントとしてサーバーでレンダリングされる**ことです。

これにより、pagesからappに移行する際に、自動的にReactサーバーコンポーネントを採用できます。

注記: サーバーコンポーネントはappフォルダまたは独自のフォルダで使用できますが、後方互換性のためにpagesディレクトリでは使用できません。

クライアントコンポーネントとサーバーコンポーネントの規約

appフォルダはサーバーコンポーネント、クライアントコンポーネント、共有コンポーネントをサポートし、これらのコンポーネントをルート内で交互に配置できるようになります。

クライアントコンポーネントとサーバーコンポーネントを定義するための正確な規約については現在議論中です。私たちはこの議論の結論に従います。

  • 現時点では、サーバーコンポーネントはファイル名に.server.jsを追加することで定義できます。例: layout.server.js
  • クライアントコンポーネントはファイル名に.client.jsを追加することで定義できます。例: page.client.js
  • .jsファイルは共有コンポーネントと見なされます。サーバーとクライアントの両方でレンダリングされる可能性があるため、各コンテキストの制約を尊重する必要があります。

注記:

  • クライアントコンポーネントとサーバーコンポーネントには守るべき制約があります。クライアントコンポーネントかサーバーコンポーネントを使うか決める際は、クライアントコンポーネントが必要になるまでサーバーコンポーネント(デフォルト)を使用することを推奨します。

フック

ヘッダーオブジェクト、クッキー、パス名、検索パラメータなどにアクセスできるクライアントコンポーネントとサーバーコンポーネントのフックを追加予定です。将来的にはより詳細な情報を含むドキュメントを提供します。

レンダリング環境

クライアントコンポーネントとサーバーコンポーネントの規約を使用して、クライアントサイドJavaScriptバンドルに含まれるコンポーネントを細かく制御できます。

デフォルトでは、app内のルートは静的生成(Static Generation)を使用し、ルートセグメントがリクエストコンテキストを必要とするサーバーサイドフックを使用すると動的レンダリングに切り替わります。

ルート内でのクライアントコンポーネントとサーバーコンポーネントの交互配置

Reactでは、サーバーコンポーネント内にサーバー専用のコード(データベースやファイルシステムユーティリティなど)が含まれる可能性があるため、クライアントコンポーネント内でサーバーコンポーネントをインポートすることに制限があります。

例えば、以下のようにサーバーコンポーネントをインポートしても動作しません:

ClientComponent.js
import ServerComponent from './ServerComponent.js';
 
export default function ClientComponent() {
  return (
    <>
      <ServerComponent />
    </>
  );
}

しかし、サーバーコンポーネントはクライアントコンポーネントの子として渡すことができます。これを行うには、それらを別のサーバーコンポーネントでラップします。例えば:

ClientComponent.js
export default function ClientComponent({ children }) {
  return (
    <>
      <h1>Client Component</h1>
      {children}
    </>
  );
}
 
// ServerComponent.js
export default function ServerComponent() {
  return (
    <>
      <h1>Server Component</h1>
    </>
  );
}
 
// page.js
// このコンポーネントはサーバーでレンダリングされるため、
// サーバーコンポーネント内でクライアントとサーバーコンポーネントをインポート可能
import ClientComponent from "./ClientComponent.js";
import ServerComponent from "./ServerComponent.js";
 
export default function ServerComponentPage() {
  return (
    <>
      <ClientComponent>
        <ServerComponent />
      </ClientComponent>
    </>
  );
}

このパターンを使用すると、Reactはサーバー専用コードを含まない結果をクライアントに送信する前に、サーバー上でServerComponentをレンダリングする必要があることを認識します。クライアントコンポーネントの観点から見ると、その子は既にレンダリング済みになります。

レイアウトでは、このパターンはchildrenプロップで適用されるため、追加のラッパーコンポーネントを作成する必要はありません。

例えば、ClientLayoutコンポーネントはServerPageコンポーネントを子として受け入れます:

app/dashboard/layout.js
// ダッシュボードレイアウトはクライアントコンポーネント
export default function ClientLayout({ children }) {
  // useState / useEffectが使用可能
  return (
    <>
      <h1>Layout</h1>
      {children}
    </>
  );
}
 
// ページはダッシュボードレイアウトに渡されるサーバーコンポーネント
// app/dashboard/settings/page.js
export default function ServerPage() {
  return (
    <>
      <h1>Page</h1>
    </>
  );
}

注記: この構成スタイルは、クライアントコンポーネント内でサーバーコンポーネントをレンダリングするための重要なパターンです。学習すべき1つのパターンの先例を設定し、childrenプロップを使用することに決めた理由の1つです。

データフェッチング

ルート内の複数のセグメントでデータをフェッチできるようになります。これはpagesディレクトリとは異なり、データフェッチングがページレベルに限定されていました。

レイアウトでのデータフェッチング

Next.jsのデータフェッチングメソッドgetStaticPropsまたはgetServerSidePropsを使用して、layout.jsファイルでデータをフェッチできます。

例えば、ブログレイアウトはgetStaticPropsを使用してCMSからカテゴリを取得し、サイドバーコンポーネントを埋めるために使用できます:

app/blog/layout.js
export async function getStaticProps() {
  const categories = await getCategoriesFromCMS();
 
  return {
    props: { categories },
  };
}
 
export default function BlogLayout({ categories, children }) {
  return (
    <>
      <BlogSidebar categories={categories} />
      {children}
    </>
  );
}

ルート内の複数のデータフェッチングメソッド

ルートの複数のセグメントでデータをフェッチすることもできます。例えば、データをフェッチするlayoutは、独自のデータをフェッチするpageをラップできます。

上記のブログの例を使用すると、単一の投稿ページはgetStaticPropsgetStaticPathsを使用してCMSから投稿データをフェッチできます:

app/blog/[slug]/page.js
export async function getStaticPaths() {
  const posts = await getPostSlugsFromCMS();
 
  return {
    paths: posts.map((post) => ({
      params: { slug: post.slug },
    })),
  };
}
 
export async function getStaticProps({ params }) {
  const post = await getPostFromCMS(params.slug);
 
  return {
    props: { post },
  };
}
 
export default function BlogPostPage({ post }) {
  return <Post post={post} />;
}

app/blog/layout.jsapp/blog/[slug]/page.jsの両方がgetStaticPropsを使用しているため、Next.jsはビルド時に/blog/[slug]ルート全体をReactサーバーコンポーネントとして静的に生成します - これによりクライアントサイドのJavaScriptが減り、ハイドレーションが速くなります。

静的に生成されたルートはこれをさらに改善します。クライアントナビゲーションはキャッシュ(サーバーコンポーネントデータ)を再利用し、作業を再計算しないため、サーバーコンポーネントのスナップショットをレンダリングしているためCPU時間が削減されます。

動作と優先順位

Next.jsのデータフェッチングメソッド(getServerSidePropsgetStaticProps)は、appフォルダ内のサーバーコンポーネントでのみ使用できます。単一のルートにまたがるセグメントでの異なるデータフェッチングメソッドは互いに影響します。

1つのセグメントでgetServerSidePropsを使用すると、他のセグメントのgetStaticPropsに影響します。getServerSidePropsセグメントのリクエストが既にサーバーに行く必要があるため、サーバーはgetStaticPropsセグメントもレンダリングします。ビルド時にフェッチされたプロップを再利用するため、データは静的のままですが、レンダリングnext build時に生成されたプロップを使用してリクエストごとにオンデマンドで行われます。

1つのセグメントで**再検証(ISR)**付きのgetStaticPropsを使用すると、他のセグメントのrevalidate付きgetStaticPropsに影響します。1つのルートに2つの再検証期間がある場合、短い再検証が優先されます。

注記: 将来的には、ルート内で完全なデータフェッチングの細分化を可能にするように最適化される可能性があります。

Reactサーバーコンポーネントでのデータフェッチング

サーバーサイドルーティング、Reactサーバーコンポーネント、Suspense、ストリーミングの組み合わせは、Next.jsでのデータフェッチングとレンダリングにいくつかの影響を与えます:

並列データフェッチング

Next.jsはウォーターフォールを最小化するために、データフェッチを並列で積極的に開始します。例えば、データフェッチングが逐次的であった場合、ルート内の各ネストされたセグメントは、前のセグメントが完了するまでデータフェッチを開始できませんでした。しかし、並列フェッチでは、各セグメントが同時にデータフェッチを開始できます。

レンダリングはコンテキストに依存する可能性があるため、各セグメントのレンダリングは、データがフェッチされ、親のレンダリングが完了した後に開始されます。

将来的には、Suspenseを使用すると、データが完全にロードされていなくても、レンダリングをすぐに開始できます。データが利用可能になる前に読み取られると、Suspenseがトリガーされます。Reactはリクエストが完了する前にサーバーコンポーネントを楽観的にレンダリングを開始し、リクエストが解決すると結果をスロットインします。

部分的なフェッチングとレンダリング

兄弟ルートセグメント間をナビゲートする場合、Next.jsはそのセグメント以下からのみフェッチおよびレンダリングします。それ以上を再フェッチまたは再レンダリングする必要はありません。これは、レイアウトを共有するページでは、ユーザーが兄弟ページ間をナビゲートする際にレイアウトが保持され、Next.jsはそのセグメント以下からのみフェッチおよびレンダリングすることを意味します。

これはReactサーバーコンポーネントにとって特に有用です。そうでなければ、各ナビゲーションでサーバー上でページ全体が再レンダリングされる代わりに、サーバー上で変更されたページの部分のみがレンダリングされます。これにより転送されるデータ量と実行時間が削減され、パフォーマンスが向上します。

例えば、ユーザーが/analyticsページと/settingsページ間をナビゲートすると、Reactはページセグメントを再レンダリングしますが、レイアウトは保持されます:

注記: ツリーの上位のデータを強制的に再フェッチすることも可能です。これがどのように見えるかの詳細についてはまだ議論中であり、RFCを更新します。

ルートグループ

appフォルダの階層はURLパスに直接マッピングされます。しかし、ルートグループを作成することでこのパターンから抜け出すことができます。ルートグループは次の目的で使用できます:

  • URL構造に影響を与えずにルートを整理する
  • レイアウトからルートセグメントを除外する
  • アプリケーションを分割して複数のルートレイアウトを作成する

規約

ルートグループはフォルダ名を括弧で囲むことで作成できます: (folderName)

注記: ルートグループの命名は組織的な目的のみであり、URLパスには影響しません。

例: レイアウトからルートを除外する

レイアウトからルートを除外するには、新しいルートグループ(例: (shop))を作成し、同じレイアウトを共有するルート(例: accountcart)をグループに移動します。グループ外のルートはレイアウトを共有しません(例: checkout)。

変更前:

変更後:

例: URLパスに影響を与えずにルートを整理する

同様に、関連するルートをまとめて整理するためにグループを作成します。括弧内のフォルダはURLから除外されます(例: (marketing)(shop))。

例: 複数のルートレイアウトを作成する

複数のルートレイアウトを作成するには、appディレクトリのトップレベルに2つ以上のルートグループを作成します。これは、完全に異なるUIやエクスペリエンスを持つセクションにアプリケーションを分割するのに便利です。各ルートレイアウトの<html><body><head>タグは個別にカスタマイズできます。

サーバー中心のルーティング

現在、Next.jsはクライアントサイドルーティングを使用しています。初期ロード後およびその後のナビゲーションでは、新しいページのリソースに対してサーバーにリクエストが行われます。これには、(特定の条件下でのみ表示されるコンポーネントを含む)すべてのコンポーネントのJavaScriptとそのプロップ(getServerSidePropsまたはgetStaticPropsからのJSONデータ)が含まれます。JavaScriptとデータの両方がサーバーからロードされると、Reactはコンポーネントをクライアントサイドでレンダリングします。

この新しいモデルでは、Next.jsはクライアントサイドの遷移を維持しながらサーバー中心のルーティングを使用します。これはサーバー上で評価されるサーバーコンポーネントと一致します。

ナビゲーション時には、データがフェッチされ、Reactはコンポーネントをサーバーサイドでレンダリングします。サーバーからの出力は、クライアント上のReactがDOMを更新するための特別な命令(HTMLやJSONではない)です。これらの命令にはレンダリングされたサーバーコンポーネントの結果が含まれるため、そのコンポーネントのJavaScriptをブラウザにロードする必要なく結果をレンダリングできます。

これは、クライアントサイドでレンダリングするためにコンポーネントのJavaScriptをブラウザに送信する現在のデフォルトのクライアントコンポーネントとは対照的です。

Reactサーバーコンポーネントを使用したサーバー中心のルーティングの利点には以下があります:

  • ルーティングはサーバーコンポーネントに使用される同じリクエストを使用します(追加のサーバーリクエストは行われません)
  • ルート間をナビゲートする際に変更されるセグメントのみをフェッチおよびレンダリングするため、サーバーでの作業が減少します
  • 新しいクライアントコンポーネントが使用されていない場合、クライアントサイドナビゲーション時にブラウザに追加のJavaScriptがロードされません
  • ルーターは新しいストリーミングプロトコルを活用するため、すべてのデータがロードされる前にレンダリングを開始できます

ユーザーがアプリ内を移動すると、ルーターはReactサーバーコンポーネントの_ペイロード_の結果をメモリ内のクライアントサイドキャッシュに保存します。キャッシュはルートセグメントごとに分割されるため、任意のレベルで無効化でき、同時レンダリング間で一貫性を確保できます。これにより、特定の場合には以前にフェッチされたセグメントのキャッシュを再利用できます。

注記

  • 静的生成とサーバーサイドキャッシュを使用してデータフェッチングを最適化できます
  • 上記の情報は後続のナビゲーションの動作を説明しています。初期ロードはHTMLを生成するためのサーバーサイドレンダリングを含む異なるプロセスです
  • クライアントサイドルーティングはNext.jsでうまく機能してきましたが、潜在的なルートの数が多い場合、クライアントがルートマップをダウンロードする必要があるため、スケールが悪くなります
  • 全体として、Reactサーバーコンポーネントを使用することで、ブラウザでロードおよびレンダリングするコンポーネントが少なくなるため、クライアントサイドナビゲーションが速くなります

インスタントローディング状態

サーバーサイドルーティングでは、ナビゲーションはデータフェッチングとレンダリングのに行われるため、データがフェッチされている間にローディングUIを表示することが重要です。そうしないとアプリケーションが反応しないように見えます。

新しいルーターは、インスタントローディング状態とデフォルトのスケルトンにSuspenseを使用します。これは、新しいセグメントのコンテンツがロードされている間にすぐにローディングUIを表示できることを意味します。サーバー上のレンダリングが完了すると、新しいコンテンツがスワップインされます。

レンダリング中:

  • 新しいルートへのナビゲーションは即座に行われます
  • 共有レイアウトは新しいルートセグメントがロードされている間もインタラクティブなままです
  • ナビゲーションは中断可能です - つまり、1つのルートのコンテンツがロードされている間にユーザーはルート間をナビゲートできます

デフォルトのローディングスケルトン

Suspense境界は、loading.jsという新しいファイル規約によって自動的にバックグラウンドで処理されます。

例:

フォルダ内にloading.jsファイルを追加することで、デフォルトのローディングスケルトンを作成できます。

loading.jsはReactコンポーネントをエクスポートする必要があります:

loading.js
export default function Loading() {
  return <YourSkeleton />
}
 
// layout.js
export default function Layout({children}) {
  return (
    <>
      <Sidebar />
      {children}
    </>
  )
}
 
// 出力
<>
  <Sidebar />
  <Suspense fallback={<Loading />}>{children}</Suspense>
</>

これにより、フォルダ内のすべてのセグメントがSuspense境界でラップされます。デフォルトのスケルトンは、レイアウトが最初にロードされた時と兄弟ページ間をナビゲートする際に使用されます。

エラーハンドリング

エラー境界は、子コンポーネントツリー内のJavaScriptエラーをキャッチするReactコンポーネントです。

規約

error.jsファイルを追加し、Reactコンポーネントをデフォルトエクスポートすることで、サブツリー内のエラーをキャッチするエラー境界を作成できます。

エラーがスローされた場合、このコンポーネントがフォールバックとして表示されます。このコンポーネントを使用して、エラーのログ記録、エラーに関する有用な情報の表示、エラーからの回復機能を実装できます。

セグメントとレイアウトのネストされた性質により、エラー境界を作成することでUIの該当部分にエラーを隔離できます。エラー発生時、境界より上のレイアウトはインタラクティブなまま状態が保持されます。

error.js
export default function Error({ error, reset }) {
  return (
    <>
      An error occurred: {error.message}
      <button onClick={() => reset()}>Try again</button>
    </>
  );
}
 
// layout.js
export default function Layout({children}) {
  return (
    <>
      <Sidebar />
      {children}
    </>
  )
}
 
// 出力
<>
  <Sidebar />
  <ErrorBoundary fallback={<Error />}>{children}</ErrorBoundary>
</>

注意:

  • error.jsと同じセグメント内のlayout.jsファイル内のエラーはキャッチされません。自動的なエラー境界はレイアウトの子をラップし、レイアウト自体はラップしないためです。

テンプレート

テンプレートは、各子レイアウトまたはページをラップする点でレイアウトと似ています。

ルート間で持続し状態を維持するレイアウトとは異なり、テンプレートは各子要素に対して新しいインスタンスを作成します。つまり、テンプレートを共有するルートセグメント間をユーザーがナビゲートする際、コンポーネントの新しいインスタンスがマウントされます。

注: 特別な理由がない限り、テンプレートではなくレイアウトを使用することを推奨します。

規約

テンプレートは、template.jsファイルからデフォルトReactコンポーネントをエクスポートすることで定義できます。コンポーネントはネストされたセグメントが入るchildrenプロップを受け入れる必要があります。

template.js
export default function Template({ children }) {
  return <Container>{children}</Container>;
}

レイアウトとテンプレートを持つルートセグメントのレンダリング出力は次のようになります:

<Layout>
  {/* テンプレートには一意のキーが与えられることに注意 */}
  <Template key={routeParam}>{children}</Template>
</Layout>

動作

共有UIをマウント/アンマウントする必要がある場合、テンプレートがより適したオプションとなります。例:

  • CSSやアニメーションライブラリを使用した入退出アニメーション
  • useEffect(ページビューのロギングなど)やuseState(ページごとのフィードバックフォームなど)に依存する機能
  • デフォルトのフレームワーク動作の変更。例: レイアウト内のSuspense境界はレイアウトが最初にロードされた時のみフォールバックを表示し、ページ切り替え時には表示しません。テンプレートでは、ナビゲーションごとにフォールバックが表示されます。

例えば、すべてのサブページを囲むボーダー付きコンテナを持つネストされたレイアウトの設計を考えます。

親レイアウト(shop/layout.js)内にコンテナを配置できます:

shop/layout.js
export default function Layout({ children }) {
  return <div className="container">{children}</div>;
}
 
// shop/page.js
export default function Page() {
  return <div>...</div>;
}
 
// shop/categories/layout.js
export default function CategoryLayout({ children }) {
  return <div>{children}</div>;
}

しかし、共有親レイアウトが再レンダリングされないため、ページ切り替え時に入退出アニメーションは再生されません。

すべてのネストされたレイアウトやページにコンテナを配置することもできます:

shop/layout.js
export default function Layout({ children }) {
  return <div>{children}</div>;
}
 
// shop/page.js
export default function Page() {
  return <div className="container">...</div>;
}
 
// shop/categories/layout.js
export default function CategoryLayout({ children }) {
  return <div className="container">{children}</div>;
}

しかし、より複雑なアプリでは、すべてのネストされたレイアウトやページに手動で配置するのは面倒でエラーが発生しやすくなります。

この規約を使用すると、ナビゲーション時に新しいインスタンスを作成するルート間でテンプレートを共有できます。つまり、DOM要素が再作成され、状態は保持されず、エフェクトが再同期されます。

高度なルーティングパターン

エッジケースをカバーし、より高度なルーティングパターンを実装できるようにする規約を導入する予定です。以下は現在検討中の例です:

インターセプトルート

場合によっては、他のルート内からルートセグメントをインターセプトすると便利です。ナビゲーション時、URLは通常通り更新されますが、インターセプトされたセグメントは現在のルートのレイアウト内に表示されます。

Before: 画像をクリックすると、独自のレイアウトを持つ新しいルートに移動します。

After: ルートをインターセプトすることで、画像をクリックすると現在のルートのレイアウト内にセグメントがロードされます(例: モーダルとして)。

/[username]セグメント内から/photo/[id]ルートをインターセプトするには、/[username]フォルダ内に/photo/[id]フォルダの複製を作成し、(..)規約を接頭辞として付けます。

規約

  • (..) - 1つ上のレベルのルートセグメント(親ディレクトリの兄弟)にマッチします。相対パスの../に似ています。
  • (..)(..) - 2つ上のレベルのルートセグメントにマッチします。相対パスの../../に似ています。
  • (...) - ルートディレクトリのルートセグメントにマッチします。

注: ページを更新または共有すると、デフォルトのレイアウトでルートがロードされます。

動的パラレルルート

場合によっては、同じビュー内で独立してナビゲート可能な2つ以上のリーフセグメント(page.js)を表示すると便利です。

例えば、同じダッシュボード内に2つ以上のタブグループがある場合を考えます。1つのタブグループをナビゲートしても、他のタブグループには影響しません。また、前後にナビゲートする際にタブの組み合わせが正しく復元される必要があります。

規約

デフォルトでは、レイアウトはネストされたレイアウトまたはページを含むchildrenプロップを受け入れます。@プレフィックスを含む名前付き「スロット」(フォルダ)を作成し、その中にセグメントをネストすることで、このプロップの名前を変更できます。

この変更後、レイアウトはchildrenではなくcustomPropというプロップを受け取ります。

analytics/layout.js
export default function Layout({ customProp }) {
  return <>{customProp}</>;
}

同じレベルに複数の名前付きスロットを追加することで、パラレルルートを作成できます。以下の例では、@views@audienceの両方がanalyticsレイアウトにプロップとして渡されます。

名前付きスロットを使用して、リーフセグメントを同時に表示できます。

analytics/layout.js
export default function Layout({ views, audience }) {
  return (
    <>
      <div>
        <ViewsNav />
        {views}
      </div>
      <div>
        <AudienceNav />
        {audience}
      </div>
    </>
  );
}

ユーザーが最初に/analyticsにナビゲートすると、各フォルダ(@views@audience)のpage.jsセグメントが表示されます。

/analytics/subscribersにナビゲートすると、@audienceのみが更新されます。同様に、/analytics/impressionsにナビゲートすると@viewsのみが更新されます。

前後にナビゲートすると、パラレルルートの正しい組み合わせが復元されます。

インターセプトとパラレルルートの組み合わせ

インターセプトとパラレルルートを組み合わせることで、アプリケーションで特定のルーティング動作を実現できます。

例えば、モーダルを作成する際、次のような一般的な課題に直面することがあります:

  • URLを通じてモーダルにアクセスできない
  • ページを更新するとモーダルが閉じる
  • 戻るナビゲーションでモーダルの背後にあるルートではなく前のルートに移動する
  • 進むナビゲーションでモーダルが再開しない

モーダルが開いた時にURLを更新し、前後ナビゲーションでモーダルを開閉したい場合があります。さらに、URLを共有する際、モーダルが開いた状態で背後にあるコンテキストと共にページをロードしたい、またはモーダルなしでコンテンツをロードしたい場合があります。

この良い例は、ソーシャルメディアサイトの写真です。通常、写真はユーザーのフィードやプロフィール内のモーダルからアクセスできます。しかし、写真を共有する際は、独自のページに直接表示されます。

規約を使用することで、モーダルの動作をデフォルトでルーティング動作にマッピングできます。

次のフォルダ構造を考えます:

このパターンでは:

  • /photo/[id]のコンテンツは、独自のコンテキスト内でURLを通じてアクセス可能です。また、/[username]ルート内からモーダル内でもアクセスできます。
  • クライアントサイドナビゲーションを使用した前後ナビゲーションでモーダルを閉じたり再開したりできます。
  • ページを更新(サーバーサイドナビゲーション)すると、モーダルを表示する代わりに元の/photo/idルートに移動します。

/@modal/(..)photo/[id]/page.jsでは、モーダルコンポーネントでラップされたページコンテンツを返せます。

/@modal/(..)photo/[id]/page.js
export default function PhotoPage() {
  const router = useRouter();
 
  return (
    <Modal
      // ページロード時には常にモーダルを表示
      isOpen={true}
      // モーダルを閉じると前のページに戻る
      onClose={() => router.back()}
    >
      {/* ページコンテンツ */}
    </Modal>
  );
}

注: このソリューションはNext.jsでモーダルを作成する唯一の方法ではありませんが、より複雑なルーティング動作を実現するために規約を組み合わせる方法を示すことを目的としています。

条件付きルート

データやコンテキストなどの動的情報を使用して、表示するルートを決定する必要がある場合があります。パラレルルートを使用して、条件に応じて1つのルートまたは別のルートをロードできます。

layout.js
export async function getServerSideProps({ params }) {
  const { accountType } = await fetchAccount(params.slug);
  return { props: { isUser: accountType === 'user' } };
}
 
export default function UserOrTeamLayout({ isUser, user, team }) {
  return <>{isUser ? user : team}</>;
}

上記の例では、スラッグに応じてuserまたはteamルートを返せます。これにより、データを条件付きでロードし、サブルートを一方または他方のオプションと照合できます。

結論

Next.jsにおけるレイアウト、ルーティング、React 18の未来に興奮しています。実装作業は開始されており、機能が利用可能になり次第発表する予定です。

コメントを残したり、GitHub Discussionsで会話に参加してください。