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

2022年5月23日(月)

レイアウト RFC

投稿者

このRFC(Request for Comment)は、2016年の導入以来、Next.jsにとって最大のアップデートの概要を示しています。

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

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

このRFCに関する作業は現在進行中で、新機能が利用可能になった時点で発表します。フィードバックを提供するには、Githubディスカッションに参加してください。

目次

動機

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

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

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

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

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

用語

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

  • ツリー:階層構造を視覚化するための規則。たとえば、親と子のコンポーネントを持つコンポーネントツリー、フォルダ構造など。
  • サブツリー ルート(最初)からリーフ(最後)までを繋ぐツリーの一部。
  • URLパス:ドメインの後に続くURLの一部。
  • URLセグメント:スラッシュで区切られたURLパスの部分。

現在のルーティングのしくみ

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

app ディレクトリの導入

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

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

ルートの定義

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

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

注記

  • このシステムでは、フォルダを使用してルートを定義し、ファイルを使用してUIを定義します(layout.jspage.js、RFCの2番目の部分ではloading.jsなどの新しいファイル規約を使用します)。
  • これにより、独自のプロジェクトファイル(UIコンポーネント、テストファイル、ストーリーなど)をappディレクトリ内に配置できます。現在、これは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フォルダ内に直接作成できます。このページには、dashboardレイアウトも適用されます。

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

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

注記

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

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

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

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

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

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サーバーコンポーネント

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

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

新しいルーティングシステムの内部では、ストリーミング、Suspense、トランジションなど、最近リリースされたReactの機能も活用されます。これらはReactサーバーコンポーネントの構成要素です。

サーバーコンポーネントのデフォルト設定

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

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

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

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

appフォルダは、サーバーコンポーネント、クライアントコンポーネント、共有コンポーネントをサポートし、ツリー内でこれらのコンポーネントを混在させることができます。

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

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

注記

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

フック

ヘッダーオブジェクト、クッキー、パス名、検索パラメーターなどにアクセスできるクライアントコンポーネントとサーバーコンポーネントのフックを追加します。 今後、詳細なドキュメントを用意する予定です。

レンダリング環境

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

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

ルートにおけるクライアントコンポーネントとサーバーコンポーネントの混在

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
// 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をレンダリングする必要があることを認識します。クライアントコンポーネントの観点からは、その子は既にレンダリングされています。

レイアウトでは、このパターンが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>
    </>
  );
}

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

データ取得

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

レイアウトにおけるデータ取得

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

たとえば、ブログレイアウトは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のデータ取得方法(`getServerSideProps`と`getStaticProps`)は、`app`フォルダ内のサーバーコンポーネントでのみ使用できます。単一ルートにまたがるセグメント内の異なるデータ取得方法は、互いに影響を与えます。

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

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

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

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)`)を作成し、同じレイアウトを共有するルート(例: `account`と`cart`)をグループに移動します。グループ外のルートはレイアウトを共有しません(例: `checkout`)。

変更前

変更後

例: URLパスに影響を与えることなくルートを整理する

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

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

複数のルートレイアウトを作成するには、`app`ディレクトリの最上位レベルに2つ以上のルートグループを作成します。これは、完全に異なるUIまたはエクスペリエンスを持つセクションにアプリケーションを分割する場合に役立ちます。各ルートレイアウトの``、``、``タグは個別にカスタマイズできます。

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

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

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

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

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

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

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

ユーザーがアプリ内を移動すると、ルーターはReactサーバーコンポーネントのペイロードの結果をインメモリクライアントサイドキャッシュに保存します。キャッシュはルートセグメントごとに分割されているため、どのレベルでも無効化でき、同時レンダリング全体で一貫性が確保されます。つまり、特定のケースでは、以前に取得されたセグメントのキャッシュを再利用できます。

注記

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

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

サーバーサイドルーティングでは、データの取得とレンダリングの後でナビゲーションが行われるため、データの取得中はローディングUIを表示することが重要です。表示しないと、アプリケーションが応答しないように見えます。

新しいルーターは、インスタントローディング状態とデフォルトのスキャフォールディング(雛形)にSuspenseを使用します。これは、新しいセグメントのコンテンツがロードされている間、ローディングUIをすぐに表示できることを意味します。サーバーでのレンダリングが完了すると、新しいコンテンツが置き換えられます。

レンダリングが行われている間

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

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

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>
</>

注記

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

テンプレート

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

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

**注記:** 特に理由がない限り、レイアウトを使用することをお勧めします。

慣例

`template.js`ファイルからデフォルトのReactコンポーネントをエクスポートすることで、テンプレートを定義できます。コンポーネントは、ネストされたセグメントで設定される`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またはアニメーションライブラリを使用したエントリ/エグジットアニメーション
  • `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は通常どおり更新されますが、インターセプトされたセグメントは現在のルートのレイアウト内に表示されます。

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

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

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

慣例

  • `(..)` - 親ディレクトリの兄弟(一つ上のルートセグメント)に一致します。相対パスでの`../`に似ています。
  • `(..)(..)` - 親ディレクトリの兄弟のさらに一つ上(二つのレベル上のルートセグメント)に一致します。相対パスでの`../../`に似ています。
  • `(...)` - ルートディレクトリのルートセグメントに一致します。

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

動的並列ルート

同じビュー内で、独立してナビゲーションできる2つ以上のリーフセグメント(`page.js`)を表示することが有用な場合があります。

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

慣例

デフォルトでは、レイアウトはネストされたレイアウトまたはページを含む`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}</>;
}

上記の例では、slugに応じてuserルートまたはteamルートのいずれかを返すことができます。これにより、データの条件付きロードが可能になり、サブルートを一方のオプションまたはもう一方のオプションに一致させることができます。

結論

レイアウト、ルーティング、React 18のNext.jsにおける将来性に期待しています。実装作業は始まっており、機能が利用可能になり次第お知らせします。

コメントを残して、GitHubディスカッションに参加してください