シングルフェッチ
シングルフェッチは、新しいデータローディング戦略とストリーミングフォーマットです。 シングルフェッチを有効にすると、Remixはクライアント側の遷移でサーバーに対して単一のHTTP呼び出しを行うようになり、複数のHTTP呼び出しを並行して(ローダーごとに1つずつ)行うことはなくなります。 さらに、シングルフェッチでは、Date
、Error
、Promise
、RegExp
など、loader
とaction
から生のオブジェクトを送信することもできます。
概要
Remixは、v2.9.0
(後にv2.13.0
でfuture.v3_singleFetch
として安定化)のfuture.unstable_singleFetch
フラグの後ろに「シングルフェッチ」(RFC)のサポートを追加しました。 これにより、この動作を選択できます。 シングルフェッチはReact Router v7でデフォルトになります。
シングルフェッチの有効化は、当初は低労力で、その後は時間をかけてすべての破壊的な変更を段階的に導入することを目的としています。 最初はシングルフェッチを有効にするために必要な最小限の変更を適用し、その後は移行ガイドを使用してアプリケーションの段階的な変更を行い、React Router v7へのスムーズで非破壊的なアップグレードを確実にします。
また、破壊的な変更も確認して、特にシリアル化とステータス/ヘッダーの動作に関する根本的な動作の変更を把握しておく必要があります。
シングルフェッチの有効化
1. 未来フラグを有効にする
2. 廃止されたfetch
ポリフィル
シングルフェッチでは、undici
をfetch
ポリフィルとして使用するか、Node 20+の組み込みfetch
を使用する必要があります。 これは、@remix-run/web-fetch
ポリフィルにはない、そこで利用可能なAPIに依存しているためです。 詳しくは、以下の2.9.0リリースノートのUndiciセクションを参照してください。
-
Node 20+を使用している場合は、
installGlobals()
への呼び出しをすべて削除し、Nodeの組み込みfetch
(これはundici
と同じです)を使用します。 -
独自のサーバーを管理していて
installGlobals()
を呼び出している場合は、undici
を使用するためにinstallGlobals({ nativeFetch: true })
を呼び出す必要があります。 -
remix-serve
を使用している場合は、シングルフェッチが有効になっていると、自動的にundici
を使用します。 -
Remixプロジェクトにminiflare/cloudflare workerを使用している場合は、互換性フラグが
2023-03-01
以降に設定されていることを確認してください。
3. 必要に応じてheaders
の実装を調整する
シングルフェッチを有効にすると、複数のローダーを実行する場合でも、クライアント側のナビゲーションで実行されるリクエストは1つだけになります。 呼び出されるハンドラーのヘッダーのマージを処理するために、headers
エクスポートは、loader
/action
データリクエストにも適用されるようになりました。 多くの場合、ドキュメントリクエストで既に持っているロジックは、新しいシングルフェッチデータリクエストに対して十分に近いはずです。
4. nonce
を追加する(CSPを使用している場合)
スクリプトのコンテンツセキュリティポリシーをnonceソースで設定している場合は、ストリーミングシングルフェッチの実装のために2つの場所にnonce
を追加する必要があります。
<RemixServer nonce={yourNonceValue}>
- これにより、クライアント側でストリーミングデータ処理を行うこのコンポーネントによってレンダリングされたインラインスクリプトにnonce
が追加されます。entry.server.tsx
のrenderToPipeableStream
/renderToReadableStream
へのoptions.nonce
パラメーターに。 Remixのストリーミングドキュメントも参照してください。
5. renderToString
を置き換える(使用している場合)
ほとんどのRemixアプリではrenderToString
は使用していませんが、entry.server.tsx
で使用することを選択している場合は、以下を読み進めてください。 そうでない場合は、この手順をスキップできます。
ドキュメントリクエストとデータリクエストの整合性を維持するために、turbo-stream
は初期ドキュメントリクエストでのデータ送信にもフォーマットとして使用されます。 つまり、シングルフェッチを選択すると、アプリケーションではrenderToString
を使用できなくなり、entry.server.tsx
でrenderToPipeableStream
やrenderToReadableStream
などのReactストリーミングレンダラーAPIを使用する必要があります。
これは、HTTPレスポンスをストリーミングする必要があるという意味ではありません。 renderToPipeableStream
のonAllReady
オプションまたはrenderToReadableStream
のallReady
プロミスを活用することで、引き続きフルドキュメントを一度に送信できます。
クライアント側では、ストリーミングされたデータはSuspense
境界でラップされているため、クライアント側のhydrateRoot
呼び出しをstartTransition
呼び出しでラップする必要があります。
破壊的な変更
シングルフェッチでは、いくつかの破壊的な変更が導入されました。 その中には、フラグを有効にしたときにすぐに処理する必要があるものもあれば、フラグを有効にした後に段階的に処理できるものもあります。 次のメジャーバージョンにアップグレードする前に、これらの変更がすべて処理されていることを確認する必要があります。
最初に処理する必要がある変更:
- 廃止された
fetch
ポリフィル: 古いinstallGlobals()
ポリフィルはシングルフェッチでは機能しません。 Node 20のネイティブfetch
APIを使用するか、カスタムサーバーでinstallGlobals({ nativeFetch: true })
を呼び出して、undiciベースのポリフィルを取得する必要があります。 headers
エクスポートがデータリクエストに適用される:headers
関数は、ドキュメントリクエストとデータリクエストの両方に適用されるようになりました。
時間をかけて処理する必要がある変更:
- 新しいストリーミングデータフォーマット: シングルフェッチは、
turbo-stream
を介して新しいストリーミングフォーマットを使用するため、JSONだけでなく、より複雑なデータをストリーミングできます。 - 自動シリアル化は不要:
loader
とaction
関数から返される生のオブジェクトは、自動的にJSONResponse
に変換されなくなり、そのままワイヤー上でシリアル化されます。 - タイプ推論の更新: 最も正確なタイプ推論を得るには、
v3_singleFetch: true
でRemixのFuture
インターフェースを拡張する必要があります。 - GETナビゲーションでのデフォルトの再検証動作はオプトアウトに変更される: 通常のナビゲーションでのデフォルトの再検証動作は、オプトインからオプトアウトに変更され、サーバーローダーはデフォルトで再実行されます。
action
再検証のオプトイン:action
の4xx
/5xx
Response
後の再検証は、オプトアウトではなく、オプトインになりました。
シングルフェッチを使用した新しいルートの追加
シングルフェッチを有効にすると、より強力なストリーミングフォーマットを活用したルートを作成できます。
v3_singleFetch: true
でRemixのFuture
インターフェースを拡張する必要があります。 詳細は、タイプ推論セクションをご覧ください。
シングルフェッチでは、ローダーから以下のデータ型を返すことができます。 BigInt
、Date
、Error
、Map
、Promise
、RegExp
、Set
、Symbol
、URL
。
シングルフェッチを使用したルートの移行
現在ローダーからResponse
インスタンス(つまり、json
/defer
)を返している場合は、シングルフェッチを活用するために、アプリコードを大幅に変更する必要はありません。
しかし、将来的にReact Router v7にアップグレードする準備を整えるために、ルート単位で以下の変更を開始することをお勧めします。 これは、ヘッダーとデータ型の更新が何も壊さないことを検証する最も簡単な方法です。
タイプ推論
シングルフェッチがない場合、loader
またはaction
から返されるプレーンなJavaScriptオブジェクトは、自動的にJSONレスポンスにシリアル化されます(json
を介して返された場合と同じです)。 タイプ推論は、これが事実であると仮定し、裸のオブジェクトの戻り値をJSONシリアル化されたものとして推論します。
シングルフェッチでは、裸のオブジェクトは直接ストリーミングされるため、シングルフェッチを選択すると、組み込みのタイプ推論は正確ではなくなります。 たとえば、Date
がクライアント側で文字列にシリアル化されると仮定されます 😕。
シングルフェッチのタイプを有効にする
シングルフェッチのタイプに切り替えるには、v3_singleFetch: true
でRemixのFuture
インターフェースを拡張する必要があります。
これは、tsconfig.json
> include
でカバーされている任意のファイルで行うことができます。
Remixプラグインのfuture.v3_singleFetch
未来フラグと併せて、vite.config.ts
で行うことをお勧めします。
これで、useLoaderData
、useActionData
、およびtypeof loader
ジェネリックを使用するその他のユーティリティは、シングルフェッチのタイプを使用するようになりました。
関数とクラスインスタンス
一般的に、関数はネットワークを介して確実に送信することはできないため、undefined
としてシリアル化されます。
メソッドもシリアル化できないため、クラスインスタンスはシリアル化可能なプロパティに絞られます。
clientLoader
とclientAction
clientLoader
の引数とclientAction
の引数の型を含めてください。 これは、クライアントデータ関数を検出する方法です。
クライアント側のローダーとアクションからのデータはシリアル化されないため、これらの型は保持されます。
ヘッダー
headers
関数は、シングルフェッチが有効になっている場合、ドキュメントリクエストとデータリクエストの両方で使用されます。 この関数を使用して、並行して実行されるローダーから返されたヘッダーをマージするか、特定のactionHeaders
を返します。
返されるレスポンス
シングルフェッチでは、Response
インスタンスを返す必要がなくなり、生のオブジェクトを直接返すことができます。 したがって、json
/defer
ユーティリティは、シングルフェッチを使用する場合は廃止されたものとして扱う必要があります。 これらは、v2の間は残るので、すぐに削除する必要はありません。 次のメジャーバージョンでは削除される可能性があるため、それまでに段階的に削除することをお勧めします。
v2では、引き続き通常のResponse
インスタンスを返し続け、status
/headers
はドキュメントリクエストと同様に(headers()
関数を介してヘッダーをマージして)有効になります。
時間をかけて、ローダーとアクションから返されるレスポンスを排除する必要があります。
loader
/action
がstatus
/headers
を設定せずにjson
/defer
を返していた場合、json
/defer
への呼び出しを削除して、データを直接返すことができます。loader
/action
がjson
/defer
を介してカスタムstatus
/headers
を返していた場合は、新しいdata()
ユーティリティを使用するように切り替える必要があります。
クライアントローダー
アプリにclientLoader
関数を使用するルートがある場合、シングルフェッチの動作がわずかに変化することに注意することが重要です。 clientLoader
は、サーバーのloader
関数の呼び出しをオプトアウトするための方法として意図されているため、シングルフェッチの呼び出しがそのサーバーのloader
を実行することは正しくありません。 しかし、すべてのローダーは並行して実行され、サーバーデータを求めているclientLoader
が実際にどれであるかを知るまで、呼び出しを待つことはできません。
たとえば、以下の/a/b/c
ルートを考えてみます。
ユーザーが/ -> /a/b/c
にナビゲートする場合、a
とb
のサーバーローダー、およびc
のclientLoader
を実行する必要があります。 これは、最終的に(またはしない可能性がありますが)独自のサーバーのloader
を呼び出します。 a
/b
のloader
を取得したいときに、c
のサーバーのloader
をシングルフェッチ呼び出しに含めることはできませんし、c
が実際にserverLoader
を呼び出す(または返す)まで遅延させることもできません。 ウォーターフォールが発生するのを防ぐためです。
したがって、clientLoader
をエクスポートするルートは、シングルフェッチをオプトアウトし、serverLoader
を呼び出すと、そのルートのサーバーloader
のみを取得するための単一のフェッチが行われます。 clientLoader
をエクスポートしていないすべてのルートは、単一のHTTPリクエストでフェッチされます。
そのため、上記のルート設定では、/ -> /a/b/c
へのナビゲーションは、a
とb
のルートに対して、最初に単一のシングルフェッチ呼び出しを行います。
GET /a/b/c.data?_routes=routes/a,routes/b
そして、c
がserverLoader
を呼び出すと、c
のサーバーのloader
のみを取得するための独自の呼び出しを行います。
GET /a/b/c.data?_routes=routes/c
リソースルート
シングルフェッチで使用される新しいストリーミングフォーマットのため、loader
とaction
関数から返される生のJavaScriptオブジェクトは、json()
ユーティリティを介して自動的にResponse
インスタンスに変換されなくなりました。 代わりに、ナビゲーションデータロードでは、他のローダーデータと組み合わせてturbo-stream
レスポンスでストリーミングされます。
これは、リソースルートにとって興味深いジレンマをもたらします。 リソースルートは、個別にヒットするように意図されており、必ずしもRemix APIを介してヒットするとは限りません。 また、他のHTTPクライアント(fetch
、cURL
など)からもアクセスできます。
リソースルートが内部のRemix APIで使用されることを目的としている場合、turbo-stream
エンコーディングを活用して、Date
やPromise
インスタンスなどのより複雑な構造をストリーミングできるようになります。 しかし、外部からアクセスされた場合、JSON構造の方が使いやすく、消費しやすいでしょう。 したがって、v2で生のオブジェクトを返した場合、その動作は少し曖昧になります。 turbo-stream
またはjson()
でシリアル化すべきでしょうか?
後方互換性を容易にし、シングルフェッチの未来フラグの導入を容易にするために、Remix v2では、Remix APIからアクセスされたか、外部からアクセスされたかによって処理されます。 将来的には、Remixでは、生のオブジェクトを外部からの消費のためにストリーミングさせたくない場合は、独自のJSONレスポンスを返す必要があります。
シングルフェッチが有効な場合のRemix v2の動作は、次のとおりです。
-
useFetcher
などのRemix APIからアクセスした場合、生のJavaScriptオブジェクトは、通常のローダーとアクションと同じように、turbo-stream
レスポンスとして返されます(これは、useFetcher
がリクエストに.data
サフィックスを追加するためです)。 -
fetch
やcURL
などの外部ツールからアクセスした場合、v2では後方互換性のために、引き続き自動的にjson()
に変換されます。- Remixは、この状況が発生した場合、非推奨の警告をログに出力します。
- 必要なときに、影響を受けるリソースルートハンドラーを更新して、
Response
オブジェクトを返すことができます。 - これらの非推奨の警告に対処することで、将来的にRemix v3へのアップグレードの準備が整います。
詳細情報
ストリーミングデータフォーマット
以前は、RemixはJSON.stringify
を使用してローダー/アクションデータをワイヤー上でシリアル化し、defer
レスポンスをサポートするためにカスタムストリーミングフォーマットを実装する必要がありました。
シングルフェッチでは、Remixは現在、turbo-stream
を内部で使用しています。 これは、ストリーミングをファーストクラスでサポートしており、JSONよりも複雑なデータを自動的にシリアル化/デシリアル化できます。 以下のデータ型は、turbo-stream
を介して直接ストリーミングできます。 BigInt
、Date
、Error
、Map
、Promise
、RegExp
、Set
、Symbol
、URL
。 Error
のサブタイプも、クライアント側でグローバルに利用可能なコンストラクターがある場合はサポートされます(SyntaxError
、TypeError
など)。
これは、シングルフェッチを有効にしたときに、コードにすぐに変更を加える必要があるかどうかに影響を与える可能性があります。
- ✅
loader
/action
関数から返されるjson
レスポンスは、引き続きJSON.stringify
を介してシリアル化されます。 したがって、Date
を返すと、useLoaderData
/useActionData
からstring
を受け取ります。 - ⚠️
defer
インスタンスまたは裸のオブジェクトを返した場合、turbo-stream
を介してシリアル化されます。 したがって、Date
を返すと、useLoaderData
/useActionData
からDate
を受け取ります。- 現在の動作(ストリーミング
defer
レスポンスを除く)を維持したい場合は、既存の裸のオブジェクトの戻り値をjson
でラップするだけです。
- 現在の動作(ストリーミング
これは、ワイヤー上でPromise
インスタンスを送信するために、defer
ユーティリティを使用する必要がなくなったことも意味します。 裸のオブジェクトにPromise
を含めることができ、useLoaderData().whatever
で取得できます。 必要に応じてPromise
をネストすることもできますが、潜在的なUXへの影響に注意してください。
シングルフェッチを採用したら、アプリケーション全体でjson
/defer
の使用を段階的に削除し、生のオブジェクトを返すようにすることをお勧めします。
ストリーミングタイムアウト
以前、Remixは、デフォルトのentry.server.tsx
ファイルに組み込まれたABORT_TIMEOUT
の概念を持っていましたが、これはReactレンダラーを終了させるものでしたが、保留中の遅延プロミスをクリーンアップするための具体的な処理は行いませんでした。
Remixが内部でストリーミングするようになったため、turbo-stream
処理をキャンセルし、保留中のプロミスを自動的に拒否し、それらのエラーをクライアントにストリーミングできます。 デフォルトでは、これは4950ms後に発生します。 これは、現在のほとんどのentry.server.tsxファイルのABORT_DELAY
である5000msより少し短い値です。 これは、Reactの処理を中止する前に、プロミスをキャンセルし、その拒否をReactレンダラーを介してストリーミングする必要があるためです。
これは、entry.server.tsx
からstreamTimeout
という数値をエクスポートすることで制御できます。 Remixは、その値をミリ秒で、loader
/action
の保留中のPromise
を拒否するまでの時間として使用します。 Reactレンダラーを中止するタイムアウトとは、この値を分離することをお勧めします。 また、Reactのタイムアウトをより長い値に設定する必要があります。 これにより、streamTimeout
からの根本的な拒否をストリーミングする時間を与えることができます。
再検証
通常のナビゲーションの動作
シングルフェッチの利点は、シンプルな精神モデルとドキュメントリクエストとデータリクエストの整合性に加えて、よりシンプルで(うまくいけば)より良いキャッシュ動作です。 一般的に、シングルフェッチではHTTPリクエストの数が減り、以前の複数フェッチの動作と比較して、キャッシュされた結果が多くなることが期待されます。
キャッシュの断片化を減らすため、シングルフェッチではGETナビゲーションでのデフォルトの再検証動作が変更されています。 以前は、Remixは、shouldRevalidate
を介してオプトインしない限り、再利用された祖先ルートのローダーを再実行しませんでした。 現在は、RemixはGET /a/b/c.data
のようなシングルフェッチリクエストの簡単なケースでは、デフォルトでこれらのローダーを再実行します。 shouldRevalidate
やclientLoader
関数が存在しない場合、これがアプリの動作になります。
いずれかのアクティブなルートにshouldRevalidate
またはclientLoader
を追加すると、_routes
パラメーターを含む、細かいシングルフェッチ呼び出しがトリガーされます。 このパラメーターは、実行するルートのサブセットを指定します。
clientLoader
が内部でserverLoader()
を呼び出すと、その特定のルートに対して別々のHTTP呼び出しがトリガーされ、以前の動作と同じになります。
たとえば、/a/b
にいて、/a/b/c
にナビゲートする場合、次のようになります。
shouldRevalidate
またはclientLoader
関数が存在しない場合:GET /a/b/c.data
- すべてのルートにローダーがあるが、
routes/a
がshouldRevalidate
を介してオプトアウトしている場合:GET /a/b/c.data?_routes=root,routes/b,routes/c
- すべてのルートにローダーがあるが、
routes/b
にclientLoader
がある場合:GET /a/b/c.data?_routes=root,routes/a,routes/c
- そして、Bの
clientLoader
がserverLoader()
を呼び出す場合:GET /a/b/c.data?_routes=routes/b
この新しい動作がアプリケーションに適していない場合は、親ルートにshouldRevalidate
を追加して、false
を返すことで、再検証しないという以前の動作にオプトインできます。 これにより、必要なシナリオでの再検証を防ぐことができます。
別のオプションは、高価な親ローダーの計算用にサーバー側のキャッシュを活用することです。
送信再検証の動作
以前は、Remixは、アクションの送信後、アクションの結果に関係なく、アクティブなすべてのローダーを再検証していました。 shouldRevalidate
を介して、ルート単位で再検証をオプトアウトできました。
シングルフェッチでは、action
が4xx/5xx
ステータスコードのResponse
を返したりスローしたりした場合、Remixはデフォルトでローダーを再検証しません。 action
が4xx/5xx
以外のResponseを返したりスローしたりした場合、再検証の動作は変わりません。 ここでは、4xx
/5xx
Responseを返すと、ほとんどの場合、実際にはデータが変更されていないため、データを再読み込みする必要がないと判断しています。
4xx/5xx
アクションレスポンス後に、1つ以上のローダーを再検証したい場合は、shouldRevalidate
関数からtrue
を返すことで、ルート単位で再検証をオプトインできます。 また、アクションのステータスコードに基づいて決定する必要がある場合は、actionStatus
パラメーターが関数に渡されます。
再検証は、シングルフェッチHTTP呼び出しの?_routes
クエリ文字列パラメーターを介して処理され、呼び出されるローダーが制限されます。 これは、細かい再検証を行っている場合、リクエストされているルートに基づいてキャッシュの列挙が発生することを意味しますが、すべての情報はURLに含まれているため、特別なCDNの設定は必要ありません(これは、CDNがVary
ヘッダーを尊重する必要があるカスタムヘッダーを介して行われた場合とは異なります)。