コンテンツにスキップ
ブログに戻る

2022年5月23日月曜日

レイアウト RFC

投稿者

このRFC(Request for Comment)は、2016年の導入以来、Next.jsにおける最大のアップデートを概説するものです。

  • ネストされたレイアウト: 複雑なアプリケーションをネストされたルートで構築。
  • Server Components 向けに設計: サブツリーナビゲーションに最適化。
  • データ取得の改善: レイアウトでウォーターフォールを回避しながらデータを取得。
  • React 18 の機能を使用: ストリーミング、Transitions、Suspense。
  • クライアントおよびサーバー ルーティング: サーバー中心のルーティングと SPA のような動作。
  • 100% インクリメンタルに採用可能: 破壊的変更なしで段階的に採用できます。
  • 高度なルーティングパターン:並列ルート、インターセプトルートなど。

新しいNext.jsルーターは、最近リリースされたReact 18の機能の上に構築されます。これらの新しい機能簡単に採用し、それらが解除するメリットを活用できるように、デフォルトと規約を導入する予定です。

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

目次

動機

GitHub、Discord、Reddit、および開発者アンケートから、Next.jsの現在のルーティングの制限に関するコミュニティからのフィードバックを収集してきました。その結果、

  • レイアウト作成の開発者エクスペリエンスは改善されるべきです。ネスト、ルート間での共有、ナビゲーション時の状態の保持が容易なレイアウトを作成できるべきです。
  • 多くのNext.jsアプリケーションはダッシュボードやコンソールであり、より高度なルーティングソリューションから恩恵を受けるでしょう。

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

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

注:一部のルーティング規約は、Server Componentsの機能の一部が元々開発された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などの新しいファイル規約を使用)。
  • これにより、独自のプロジェクトファイル(UIコンポーネント、テストファイル、ストーリーなど)をappディレクトリ内にコロケートできます。現在、これはpageExtensions configでのみ可能です。

ルートセグメント

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

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

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

ルートセグメントという名前は、URLパスに関する既存の用語に合わせるために選ばれました。

レイアウト

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

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

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

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

レイアウトには2種類あります。

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

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

ルートレイアウト

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

注意

  • ルートレイアウトは、すべてのルートに適用されるため、カスタムApp (_app.js)およびカスタムドキュメント (_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プロップは子レイアウト(サブツリーのさらに下にある場合)またはページで populatED されます。

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

app/layout.js
// Root layout
// - Applies to all routes
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}
app/dashboard/layout.js
// Regular layout
// - Applies to route segments in app/dashboard/*
export default function DashboardLayout({ children }) {
  return (
    <>
      <DashboardSidebar />
      {children}
    </>
  );
}
app/dashboard/analytics/page.js
// Page Component
// - The UI for the `app/dashboard/analytics` segment
// - Matches the `acme.com/dashboard/analytics` URL path
export default function AnalyticsPage() {
  return <main>...</main>;
}

上記のレイアウトとページの組み合わせにより、以下のコンポーネント階層がレンダリングされます。

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

React Server Components

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

このRFCにより、Reactの機能を使用開始し、Next.jsアプリケーションにReact Server Componentsを段階的に採用できます。

新しいルーティングシステムの内部でも、ストリーミング、Suspense、Transitionsのような最近リリースされたReactの機能が活用されます。これらはReact Server Componentsのビルディングブロックです。

Server Componentsのデフォルト化

pagesディレクトリとappディレクトリの最大の違いは、デフォルトでは、app内のファイルはReact Server Componentsとしてサーバーでレンダリングされることです。

これにより、pagesからappへの移行時にReact Server Componentsを自動的に採用できます。

注:Server Componentsは、appフォルダまたは独自のフォルダで使用できますが、後方互換性のためにpagesディレクトリでは使用できません。

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

appフォルダは、サーバー、クライアント、共有コンポーネントをサポートし、これらのコンポーネントをツリーにインターリーブできます。

Client ComponentsとServer Componentsを定義するための正確な規約については、現在議論中です。この議論の解決に従います。

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

注意

  • Client ComponentsとServer Componentsには 制約 があり、これを尊重する必要があります。クライアントコンポーネントまたはサーバーコンポーネントを使用する決定を下す際には、クライアントコンポーネントが必要になるまでサーバーコンポーネント(デフォルト)の使用をお勧めします。

Hooks

ヘッダーオブジェクト、Cookie、パス名、検索パラメータなどにアクセスできるClient ComponentsおよびServer Componentsのフックを追加する予定です。将来的には、より詳しい情報を含むドキュメントを提供する予定です。

レンダリング環境

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

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

ルートにおけるクライアントコンポーネントとサーバーコンポーネントのインターリーブ

Reactでは、Server ComponentsをClient Components内にインポートすることには制限があります。これは、Server Componentsがサーバー専用のコード(例:データベースまたはファイルシステムユーティリティ)を持つ可能性があるためです。

例えば、Server Componentsをインポートしても機能しません。

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

しかし、Server ComponentはClient Componentの子として渡すことができます。これは、別のServer Componentでラップすることによって実現できます。例えば

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
// It's possible to import Client and Server components inside Server Components
// because this component is rendered on the server
import ClientComponent from "./ClientComponent.js";
import ServerComponent from "./ServerComponent.js";
 
export default function ServerComponentPage() {
  return (
    <>
      <ClientComponent>
        <ServerComponent />
      </ClientComponent>
    </>
  );
}

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

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

例えば、ClientLayoutコンポーネントはServerPageコンポーネントを子として受け取ります。

app/dashboard/layout.js
// The Dashboard Layout is a Client Component
export default function ClientLayout({ children }) {
  // Can use useState / useEffect here
  return (
    <>
      <h1>Layout</h1>
      {children}
    </>
  );
}
 
// The Page is a Server Component that will be passed to Dashboard Layout
// app/dashboard/settings/page.js
export default function ServerPage() {
  return (
    <>
      <h1>Page</h1>
    </>
  );
}

注:この構成スタイルは、Client Components内でServer Componentsをレンダリングするための重要なパターンです。これは、学習すべき1つのパターンの優先順位を設定し、childrenプロップを使用する理由の1つです。

データ取得

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

レイアウトでのデータ取得

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

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

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をラップすることもできます。

上記のブログの例を使用して、単一の投稿ページは、CMSから投稿データを取得するためにgetStaticPropsgetStaticPathsを使用できます。

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 Server Componentsとして静的に生成します。これにより、クライアントサイドJavaScriptが削減され、高速なハイドレーションが得られます。

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

動作と優先順位

Next.jsのデータ取得メソッド(getServerSidePropsおよびgetStaticProps)は、appフォルダのServer Componentsでのみ使用できます。単一ルートにわたるセグメントの異なるデータ取得メソッドは互いに影響します。

あるセグメントでgetServerSidePropsを使用すると、他のセグメントのgetStaticPropsに影響します。getServerSidePropsセグメントのために既にサーバーへのリクエストが行われているため、サーバーはgetStaticPropsセグメントもレンダリングします。ビルド時に取得されたpropsを再利用するため、データは静的になります。レンダリングは、next build中に生成されたpropsを使用して、各リクエストでオンデマンドで行われます。

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

注:将来的には、ルートでの完全なデータ取得粒度を可能にするために最適化される可能性があります。

React Server Componentsでのデータ取得

サーバーサイドルーティング、React Server Components、Suspense、およびストリーミングの組み合わせは、Next.jsでのデータ取得とレンダリングにいくつかの影響を与えます。

パラレルなデータ取得

Next.jsは、ウォーターフォールを最小限に抑えるために、データ取得を並列的に積極的に開始します。例えば、データ取得がシーケンシャルだった場合、ネストされた各セグメントは前のセグメントが完了するまでデータの取得を開始できませんでした。しかし、並列取得では、各セグメントが同時にデータ取得を積極的に開始できます。

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

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

部分的な取得とレンダリング

兄弟ルートセグメント間をナビゲートする際、Next.jsはそれより上の部分を再取得または再レンダリングする必要はなく、そのセグメントから下のみを取得およびレンダリングします。これは、レイアウトを共有するページでは、ユーザーが兄弟ページ間をナビゲートするときにレイアウトが保持され、Next.jsはそれより下のセグメントからのみ取得およびレンダリングすることを意味します。

これはReact Server Componentsにとって特に役立ちます。そうでない場合、各ナビゲーションでサーバー全体が再レンダリングされるのではなく、サーバーでページの変更された部分のみがレンダリングされます。これにより、転送されるデータ量と実行時間が削減され、パフォーマンスが向上します。

例えば、ユーザーが/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(特定の条件下でのみ表示されるコンポーネントを含む)とそのprops(getServerSidePropsまたはgetStaticPropsからのJSONデータ)が含まれます。JavaScriptとデータの両方がサーバーからロードされると、Reactはコンポーネントをクライアントサイドでレンダリングします

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

ナビゲーション時、データが取得され、Reactはコンポーネントをサーバーサイドでレンダリングします。サーバーからの出力は、クライアント上のReactがDOMを更新するための特別な指示(HTMLやJSONではない)です。これらの指示には、レンダリングされたServer Componentsの結果が含まれており、結果をレンダリングするためにそのコンポーネントのJavaScriptをブラウザにロードする必要はありません。

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

React Server Componentsを使用したサーバー中心のルーティングの利点のいくつか:

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

ユーザーがアプリ内をナビゲートする際、ルーターはメモリ内クライアントサイドキャッシュにReact Server Componentの*ペイロード*の結果を保存します。キャッシュはルートセグメントごとに分割されており、任意のレベルでの無効化が可能で、同時レンダリング間の一貫性を確保します。これは、特定のケースでは、以前に取得されたセグメントのキャッシュを再利用できることを意味します。

注意

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

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

サーバーサイドルーティングでは、ナビゲーションはデータ取得とレンダリングのに行われるため、データが取得されている間にローディング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}
    </>
  )
}
 
// Output
<>
  <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}
    </>
  )
}
 
// Output
<>
  <Sidebar />
  <ErrorBoundary fallback={<Error />}>{children}</ErrorBoundary>
</>

注意

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

テンプレート

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

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

注:特定の理由がない限り、テンプレートの使用は推奨されません。

規約

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

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

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

<Layout>
  {/* Note that the template is given a unique key. */}
  <Template key={routeParam}>{children}</Template>
</Layout>

動作

共有UIをマウントおよびアンマウントする必要がある場合があります。その場合、テンプレートがより適切なオプションになります。例えば:

  • CSSまたはアニメーションライブラリを使用したEnter/Exitアニメーション
  • 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>;
}

しかし、共有親レイアウトが再レンダリングされないため、ページを切り替えるときにEnter/Exitアニメーションは再生されません。

コンテナをすべてのネストされたレイアウトまたはページに配置できます。

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は通常どおり更新されますが、インターセプトされたセグメントは現在のルートのレイアウト内に表示されます。

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

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

/[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
      // the modal should always be shown on page load
      isOpen={true}
      // closing the modal should take user back to the previous page
      onClose={() => router.back()}
    >
      {/* Page Content */}
    </Modal>
  );
}

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

条件付きルート

データやコンテキストのような動的な情報に基づいて表示するルートを決定する必要がある場合があります。並列ルートを使用して、一方のルートまたはもう一方を条件付きでロードできます。

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ルートを返すことができます。これにより、データを条件付きでロードし、サブルートを一方または他方に一致させることができます。

結論

レイアウト、ルーティング、およびReact 18の将来のNext.jsでの展開に期待しています。実装作業が開始されており、機能が利用可能になったら発表します。

コメントを残して、GitHub Discussionsで議論に参加してください。