原文アドレス:Lazy Loading Routes in React Router 6.4+
React Router 6.4 + における遅延読み込みルート#
React Router 6.4 は「データルーター」の概念を導入し、主な焦点はデータ取得とレンダリングを分離することで、レンダリング + 取得のチェーンとそれに伴うスピナーを排除することです。
これらのチェーンは通常「滝」と呼ばれますが、私たちはこの用語を再考しています。なぜなら、多くの人が「滝」と聞くとナイアガラの滝を想像し、すべての水が美しい大滝に落ちることを思い浮かべるからです。しかし、「一度に」データを読み込むのは良い方法のように思えますが、なぜ滝を嫌う必要があるのでしょうか?もしかしたら、私たちはそれを追求すべきなのでしょうか?
実際に避けたい「滝」は、上のタイトル画像のように見え、階段に似ています。水が少し流れ、止まり、また少し流れ、止まり、という具合です。今、その階段の各ステップにローディングスピナーがあることを想像してみてください。これは私たちがユーザーに提供したい UI のタイプではありません!したがって、この記事(およびそれ以外の希望)では、「チェーン」という用語を使用して、固有の順序で取得され、各取得が前の取得によってブロックされることを示します。
レンダー + 取得チェーン#
もしあなたがまだ「Remixing React Router」の記事や、昨年の Reactathon での Ryan の「When to Fetch」の講演を読んでいないなら、この記事を深く読む前にそれらを確認することをお勧めします。これらはデータルーティングの概念を導入する背景を多くカバーしています。
簡単に言えば、ルーターがデータのニーズを知らないと、最終的にはリンクリクエストが発生し、子コンポーネントをレンダリングする際に「発見」された後続のデータニーズが生じます。
しかし、データルーターを導入することで、データを並行して取得し、すべてを一度にレンダリングすることができます:
これを実現するために、データルーターはルート定義をレンダリングサイクルから抽出し、私たちのルーターがネストされたデータニーズを事前に認識できるようにします。
// app.jsx
import Layout, { getUser } from `./layout`
import Home from `./home`
import Projects, { getProjects } from `./projects`
import Project, { getProject } from `./project`
const routes = [
{
path: '/',
loader: () => getUser(),
element: <Layout />,
children: [
{
index: true,
element: <Home />,
},
{
path: 'projects',
loader: () => getProjects(),
element: <Projects />,
children: [
{
path: ':projectId',
loader: ({ params }) => getProject(params.projectId),
element: <Project />,
},
],
},
],
},
]
しかし、これには欠点もあります。これまでのところ、データ取得を最適化する方法について議論してきましたが、JS バンドルの取得を最適化する方法も考慮する必要があります!上記のルート定義により、すべてのデータを並行して取得できる一方で、すべてのローダーとコンポーネントを含む JavaScript バンドルをダウンロードする必要があるため、データ取得の開始がブロックされます。
ユーザーが / ルートであなたのウェブサイトに入ると仮定してみてください:
React.lazy は私たちを救うことができるか?#
React.lazyはコンポーネントツリーを分割するための優れた原語を提供しますが、データルーターが排除しようとしている取得とレンダリングの密接な結合という同じ問題に悩まされています 😕。これは、React.lazy () を使用すると、コンポーネントの非同期チャンクを作成しますが、React は遅延コンポーネントをレンダリングするまでそのチャンクの取得を開始しないためです。
// app.jsx
const LazyComponent = React.lazy(() => import('./component'))
function App() {
return (
<React.Suspense fallback={<p>遅延チャンクを読み込んでいます...</p>}>
<LazyComponent />
</React.Suspense>
)
}
したがって、私たちはデータルーター内で React.lazy () を活用できますが、最終的にはコンポーネントをダウンロードするためのチェーンを導入することになります。Ruben Casas は素晴らしい記事を書き、データルーター内で React.lazy () を使用してコード分割を行ういくつかの方法を紹介しています。しかし、この記事からは、手動でコード分割を行うのは依然として少し冗長で面倒であることがわかります。DX が十分でないため、@rossipedia からの提案(および初期のPOC 実装)を受け取りました。この提案は、現在直面している課題をうまく概説しており、RouterProvider に一流のコード分割サポートを導入する方法を考え始めるきっかけとなりました。この 2 人(および他の素晴らしいコミュニティメンバー)に大きな感謝を捧げ、React Router の進化に積極的に参加してくれたことに感謝します 🙌。
Route.lazy の紹介#
もし私たちが遅延読み込みをデータルーターと良好に組み合わせたいのであれば、レンダリングサイクルの外で遅延を導入できる必要があります。データ取得をレンダリングサイクルから抽出したように、ルート取得も抽出したいと考えています。
一歩引いてルート定義を見ると、それは 3 つの部分に分けることができます:
- パスマッチフィールド、例えばパス、インデックス、子ノード
- データの読み込み / 送信フィールド、例えばローダーとアクション
- レンダリングフィールド、例えば要素とエラー要素
データルーターが重要なパスで本当に必要とするのはマッチパスフィールドです。なぜなら、それは与えられた URL にマッチするすべてのルートを認識できる必要があるからです。マッチした後、非同期ナビゲーションが進行中であるため、そのナビゲーション中にルート情報を取得できない理由はありません。そして、データ取得が完了するまでレンダリングの内容は必要ありません。データ取得が完了するまでターゲットルートを表示しないからです。はい、これは「チェーン」の概念(ルートを読み込み、その後データを読み込む)を導入する可能性がありますが、これはオプションのレバレッジであり、初期読み込み速度とその後のナビゲーション速度のトレードオフを解決するために必要に応じて引き出すことができます。
以下は、上記のルート構造を使用し、新しい lazy () メソッド(React Router v6.9.0 で利用可能)をルート定義の中で使用する例です:
// app.jsx
import Layout, { getUser } from `./layout`;
import Home from `./home`;
const routes = [{
path: '/',
loader: () => getUser(),
element: <Layout />,
children: [{
index: true,
element: <Home />,
}, {
path: 'projects',
lazy: () => import("./projects"), // 💤 遅延読み込み!
children: [{
path: ':projectId',
lazy: () => import("./project"), // 💤 遅延読み込み!
}],
}],
}]
// projects.jsx
export function loader = () => { ... }; // 以前はgetProjectsと呼ばれていました
export function Component() { ... } // 以前はProjectsと呼ばれていました
// project.jsx
export function loader = () => { ... }; // 以前はgetProjectと呼ばれていました
export function Component() { ... } // 以前はProjectと呼ばれていました
あなたは機能コンポーネントをエクスポートしているのですか?この遅延モジュールからエクスポートされたプロパティは、ルート定義に逐次追加されます。要素をエクスポートするのは奇妙なので、要素ではなくコンポーネントをルートオブジェクトに定義するサポートを追加しました(しかし心配しないでください、要素はまだ使用できます!)。
この場合、レイアウトとホームルートを主要なバンドルに残すことを選択しました。なぜなら、これはユーザーが最もよく使用する入り口だからです。しかし、プロジェクトのインポートと:projectId ルートのインポートをそれぞれの動的インポートに移動しました。これにより、それらのルートにナビゲートしない限り、読み込まれません。
初期読み込み時に生成されるネットワークグラフは大体次のようになります:
現在、私たちの重要なパスバンドルには、ウェブサイトに入るために最も重要だと考えられるルートのみが含まれています。そして、ユーザーが /projects/123 へのリンクをクリックすると、lazy () メソッドを通じてこれらのルートを並行して取得し、それらが返すローダーメソッドを実行します:
これにより、私たちはある程度の両立を実現しました。なぜなら、私たちは重要なパスを関連するホームルートにバンドルすることができたからです。そしてナビゲーション中に、パスをマッチさせて必要な新しいルート定義を取得できます。
高度な使用法と最適化#
敏感な読者の中には、ここにいくつかの🕷️スパイダーマンのような直感が隠されていると感じるかもしれません。これは最適なネットワークグラフですか?実際にはそうではありません!しかし、私たちがあまり多くのコードを書かずにこれを得たことを考えると、かなり良いです 😉。
上記の例では、私たちのルートモジュールにはローダーとコンポーネントが含まれているため、ローダーを開始する前に両方の内容をダウンロードする必要があります。実際、React Router SPA では、ローダーは通常非常に小さく、外部 API にアクセスし、その大部分のビジネスロジックが存在します。一方、コンポーネントは全体のユーザーインターフェースを定義し、それに関連するすべてのユーザーインタラクションを含んでいます - それらはかなり大きくなる可能性があります。
ローダー(おそらくある API に対して fetch () 呼び出しを行っている可能性がある)が大きなコンポーネントツリーを JS でダウンロードするのを妨げるのは愚かに思えます。もし私たちがこれ👆をこれ👇に変えることができたらどうなるでしょう?
良いニュースは、最小限のコード変更で実現できることです!ルートに静的にローダー / アクションを定義すると、それは lazy () と並行して実行されます。これにより、ローダーとコンポーネントを異なるファイルに分けることで、ローダーデータ取得とコンポーネントチャンクのダウンロードを分離できます:
const routes = [
{
path: 'projects',
async loader({ request, params }) {
let { loader } = await import('./projects-loader')
return loader({ request, params })
},
lazy: () => import('./projects-component'),
},
]
ルートに静的に定義されたフィールドは、常に lazy から返された内容よりも優先されます。したがって、静的ローダーを定義し、lazy からローダーを返すべきではありませんが、もしそうした場合、遅延バージョンは無視され、コンソール警告が表示されます。
この静的定義されたローダーの概念は、直接インラインコードのいくつかの興味深い可能性を開きます。たとえば、リクエスト URL に基づいて特定のルートのデータを取得する方法を知っている別の API エンドポイントがあるかもしれません。最小限のバンドルコストで、すべてのローダーをインライン化し、データ取得とコンポーネント(またはルートモジュール)チャンクのダウンロードの間で完全な並行化を実現できます。
const routes = [
{
path: 'projects',
loader: ({ request }) => fetchDataForUrl(request.url),
lazy: () => import('./projects-component'),
},
]