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

2022年5月23日月曜日

レイアウトRFC

投稿者

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

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

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

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

目次

動機

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

  • レイアウト作成の開発者エクスペリエンスは改善できる。ネスト可能で、複数のルートで共有でき、ナビゲーション時に状態が保持されるレイアウトを簡単に作成できるべきです。
  • 多くの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の第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ファイルを、サブツリーの上位に存在するレイアウトでラップしてレンダリングします。

ダッシュボードフォルダ内に直接page.jsファイルを作成することで、/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 Server ComponentsをNext.jsアプリケーションに段階的に採用できます。

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

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

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

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

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

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

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

クライアントコンポーネントとサーバーコンポーネントの定義に関する規約が正確にどうなるかについては、現在も議論が進行中です。この議論の結果に従います。

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

注意

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

Hooks

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

レンダリング環境

クライアントコンポーネントとサーバーコンポーネントの規約を使用して、クライアントサイドの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ディレクトリとは異なります。

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

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

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

動作と優先順位

Next.jsのデータ取得メソッド(getServerSidePropsおよびgetStaticProps)は、appフォルダ内のサーバーコンポーネントでのみ使用できます。単一ルート内のセグメントで異なるデータ取得メソッドを使用すると、互いに影響を及ぼします。

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

1つのセグメントでrevalidate (ISR) とともにgetStaticPropsを使用すると、他のセグメントのrevalidateととともにgetStaticPropsに影響を与えます。1つのルートに2つのrevalidate期間がある場合、より短いrevalidationが優先されます。

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

Reactサーバーコンポーネントでのデータ取得

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

並列データ取得

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

レンダリングはContextに依存する場合があるため、各セグメントのレンダリングは、データが取得され、親のレンダリングが完了した後に開始されます。

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

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

兄弟ルートセグメント間を移動する際、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と、そのプロップ(getServerSidePropsまたはgetStaticPropsからのJSONデータ)が含まれます。JavaScriptとデータがサーバーからロードされると、**Reactはクライアントサイドでコンポーネントをレンダリングします。**

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

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

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

Reactサーバーコンポーネントを使用したサーバー中心のルーティングには、いくつかの利点があります。

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

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

注意

  • 静的生成とサーバーサイドキャッシュは、データ取得を最適化するために使用できます。
  • 上記の情報は、その後のナビゲーションの動作について説明しています。初期ロードは、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>
</>

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

エラーハンドリング

エラー境界は、子コンポーネントツリー内のどこかで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(例: ページごとのフィードバックフォーム)に依存する機能
  • デフォルトのフレームワーク動作を変更する。例: レイアウト内のサスペンス境界は、レイアウトが最初にロードされたときのみフォールバックを表示し、ページ切り替え時には表示しません。テンプレートの場合、ナビゲーションごとにフォールバックが表示されます。

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

コンテナを親レイアウト(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]フォルダを作成し、(..)規約をプレフィックスとして付けます。

規約

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

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

動的並列ルート

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

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

規約

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

この変更後、レイアウトはchildrenの代わりにcustomPropという名前のプロップを受け取ります。

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

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

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

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ルートのいずれかを返すことができます。これにより、データを条件付きでロードし、サブルートをどちらかのオプションと一致させることができます。

結論

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

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