シングルフェッチ
シングルフェッチは、新しいデータのデータ読み込み戦略とストリーミング形式です。シングルフェッチを有効にすると、Remixはクライアント側の遷移時にサーバーに対して単一のHTTP呼び出しを行うようになり、複数のHTTP呼び出しを並行して行う(ローダーごとに1回)ことはありません。さらに、シングルフェッチでは、Date
、Error
、Promise
、RegExp
など、loader
とaction
から裸のオブジェクトをダウンストリーム送信することもできます。
概要
Remixは、v2.9.0
の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
が使用されます。 -
miniflare/cloudflare workerをRemixプロジェクトで使用している場合は、互換性フラグが
2023-03-01
以降に設定されていることを確認してください。
3. ドキュメントレベルのheaders
実装を削除する(存在する場合)
シングルフェッチが有効になっている場合は、headers
エクスポートは使用されなくなります。多くの場合、ローダーのResponse
インスタンスからヘッダーを再返却してドキュメントリクエストに適用していた可能性があり、その場合はエクスポートを削除するだけで、これらのRepsonseヘッダーがドキュメントリクエストに自動的に適用されます。headers
関数でより複雑なロジックをドキュメントヘッダーに適用していた場合は、loader
関数内の新しいResponseスタブインスタンスにそれらを移行する必要があります。
4. <RemixServer>
にnonce
を追加する(CSPを使用している場合)
<RemixServer>
コンポーネントは、クライアント側でストリーミングデータを処理するインラインスクリプトをレンダリングします。スクリプトのコンテンツセキュリティポリシーをnonceソースとともに使用している場合は、<RemixServer nonce>
を使用してnonceをこれらの<script>
タグに渡すことができます。
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()
ポリフィルはシングルフェッチでは機能しません。undiciベースのポリフィルを取得するには、ネイティブのNode 20のfetch
APIを使用するか、カスタムサーバーでinstallGlobals({ nativeFetch: true })
を呼び出す必要があります。 - 廃止された
headers
エクスポート:headers
関数は、シングルフェッチが有効になっている場合は使用されなくなり、代わりにloader
/action
関数に渡される新しいresponse
スタブが使用されます。
時間をかけて処理する必要があることを理解しておくべき変更:
- 新しいストリーミングデータ形式: シングルフェッチは、
turbo-stream
を介して新しいストリーミング形式を内部で使用します。これは、JSONよりも複雑なデータをストリーミングできることを意味します。 - 自動シリアル化なし:
loader
とaction
関数から返された裸のオブジェクトは、もはや自動的にJSONResponse
に変換されず、ワイヤー上でそのままシリアル化されます。 - タイプ推論の更新: 最も正確なタイプ推論を得るためには、次の2つのことを行う必要があります。
tsconfig.json
のcompilerOptions.types
配列の最後に@remix-run/react/future/single-fetch.d.ts
を追加します。- ルートで
unstable_defineLoader
/unstable_defineAction
を使用し始めます。- これは段階的に行うことができます。現在の状態では、ほとんどの場合、正確なタイプ推論が得られます。
- オプトインの
action
再検証:action
の4xx
/5xx
Response
後の再検証は、オプトアウトではなく、オプトインになりました。
シングルフェッチを使用した新しいルートの追加
シングルフェッチが有効になっている場合、より強力なストリーミング形式とresponse
スタブを活用するルートを作成することができます。
tsconfig.json
のcompilerOptions.types
配列の最後に@remix-run/react/future/single-fetch.d.ts
を追加する必要があります。これについては、タイプ推論セクションで詳しく説明します。
シングルフェッチでは、BigInt
、Date
、Error
、Map
、Promise
、RegExp
、Set
、Symbol
、URL
などのデータ型をローダーから返すことができます。
シングルフェッチを使用したルートの移行
現在ローダーからResponse
インスタンス(つまり、json
/defer
)を返している場合は、シングルフェッチの利点を活用するためにアプリケーションコードを変更する必要はありません。
ただし、将来的にReact Router v7にアップグレードする準備として、ルートごとに以下の変更を行うことをお勧めします。これは、ヘッダーとデータ型を更新しても何も壊れないことを検証する最も簡単な方法です。
型推論
シングルフェッチなしでは、loader
またはaction
から返されたプレーンなJavaScriptオブジェクトは自動的にJSONレスポンスにシリアル化されます(json
を介して返された場合と同じです)。タイプ推論では、これが事実であると想定され、裸のオブジェクトの返却値は、JSONシリアル化された場合と同じように推論されます。
シングルフェッチでは、裸のオブジェクトは直接ストリーミングされるため、シングルフェッチを選択すると、組み込みのタイプ推論はもはや正確ではありません。たとえば、Date
はクライアント側で文字列にシリアル化されると想定されます😕。
シングルフェッチを使用するときに適切なタイプを取得するには、tsconfig.json
のcompilerOptions.types
配列に含めることができる、一連のタイプオーバーライドを用意しました。これにより、タイプがシングルフェッチの動作と一致するようになります。
🚨 シングルフェッチのタイプは、types
内の他のRemixパッケージの後にあることを確認してください。これにより、既存のタイプをオーバーライドできます。
ローダー/アクション定義ユーティリティ
シングルフェッチを使用してローダーとアクションを定義する場合、タイプセーフティを高めるために、新しいunstable_defineLoader
とunstable_defineAction
ユーティリティを使用できます。
これにより、引数のタイプが得られるだけでなく(LoaderFunctionArgs
は廃止されます)、シングルフェッチと互換性のあるタイプを返していることも保証されます。
シングルフェッチは、以下の返却タイプをサポートしています。
defineClientLoader
/defineClientAction
というクライアント側の同等のユーティリティもあります。これらは、clientLoader
/clientAction
から返されるデータはワイヤー上でシリアル化する必要がないため、同じ返却値の制限はありません。
useLoaderData
とその同等のユーティリティに対するタイプ推論のためです。特定のResponse
を返し、Remix API(useFetcher
など)では使用されないリソースルートがある場合は、通常のloader
/action
定義のままにすることができます。これらのルートをdefineLoader
/defineAction
を使用して変換すると、turbo-stream
はResponse
インスタンスをシリアル化できないため、タイプエラーが発生します。
useLoaderData
、useActionData
、useRouteLoaderData
、useFetcher
これらのメソッドは、コードの変更を必要としません。シングルフェッチのタイプを追加すると、ジェネリックが正しく逆シリアル化されます。
useMatches
useMatches
では、手動でキャストしてローダータイプを指定する必要があります。これにより、match.data
に対する適切なタイプ推論が得られます。シングルフェッチを使用する場合は、UIMatch
タイプをUIMatch_SingleFetch
に置き換える必要があります。
meta
関数
meta
関数も、現在のルートローダーと祖先ルートローダーのタイプを示すジェネリックを必要とし、これによりdata
とmatches
パラメーターが正しく型付けされます。シングルフェッチを使用する場合は、MetaArgs
タイプをMetaArgs_SingleFetch
に置き換える必要があります。
ヘッダー
headers
関数は、シングルフェッチが有効になっている場合は使用されなくなります。
代わりに、loader
/ action
関数には、その実行に固有の変更可能なResponseStub
が渡されます。
- HTTPレスポンスのステータスを変更するには、
status
フィールドを直接設定します。response.status = 201
- HTTPレスポンスのヘッダーを設定するには、標準の
Headers
APIを使用します。response.headers.set(name, value)
response.headers.append(name, value)
response.headers.delete(name)
これらのレスポンススタブをスローして、ローダーとアクションのフローを短絡させることもできます。
各loader
/action
には、それぞれ固有のresponse
インスタンスが渡されるため、他のloader
/action
関数で設定された内容を確認することはできません(これは競合状態が発生する可能性があります)。結果として得られるHTTPレスポンスのステータスとヘッダーは、以下のように決定されます。
- ステータスコード
- すべてのステータスコードが設定されていないか、値が<300の場合は、最も深いステータスコードがHTTPレスポンスに使用されます。
- すべてのステータスコードが設定されているか、値が>=300の場合は、最も浅い>=300の値がHTTPレスポンスに使用されます。
- ヘッダー
- Remixはヘッダー操作を追跡し、すべてのハンドラーが完了した後に新しい
Headers
インスタンスでそれらを再生します。 - これらは、順序どおりに、最初にアクション(存在する場合)、次に上から下にローダーの順で再生されます。
- 子ハンドラーの
headers.set
は、親ハンドラーの値を上書きします。 headers.append
を使用すると、親ハンドラーと子ハンドラーの両方から同じヘッダーを設定できます。headers.delete
を使用すると、親ハンドラーで設定された値を削除できますが、子ハンドラーで設定された値を削除することはできません。
- Remixはヘッダー操作を追跡し、すべてのハンドラーが完了した後に新しい
シングルフェッチは裸のオブジェクトの返却をサポートしており、ステータス/ヘッダーを設定するためにResponse
インスタンスを返す必要がなくなったため、json
/redirect
/redirectDocument
/defer
ユーティリティは、シングルフェッチを使用する場合は廃止されているとみなしてください。これらはv2の間は残りますが、すぐに削除する必要はありません。次のメジャーバージョンでは、おそらく削除されるため、できるだけ早く段階的に削除することをお勧めします。
これらのユーティリティは、Remix v2の残りの期間は引き続き使用できます。今後のバージョンでは、remix-utils
などのものから利用できるようになる可能性があります(または、自分で簡単に再実装することもできます)。
v2では、引き続き通常のResponse
インスタンスを返すことができ、response
スタブと同じ方法でステータスコードが適用され、すべてのヘッダーがheaders.set
を介して適用され、親からの同じ名前のヘッダーの値は上書きされます。ヘッダーを追加する必要がある場合は、Response
インスタンスを返すことから、新しいresponse
パラメーターを使用するように切り替える必要があります。
これらの機能を段階的に採用できるようにするために、シングルフェッチを有効にしても、すべてのloader
/action
関数を変更してresponse
スタブを利用する必要はありません。その後、時間をかけて個々のルートを段階的に変換して、新しいresponse
スタブを利用することができます。
クライアントローダー
アプリケーションでclientLoader
関数を使用するルートがある場合は、シングルフェッチの動作がわずかに変化することに注意することが重要です。clientLoader
は、サーバーのloader
関数の呼び出しをオプトアウトする方法を提供することを目的としているため、シングルフェッチの呼び出しでそのサーバーローダーを実行することは正しくありません。しかし、すべてのローダーは並行して実行され、どのclientLoader
が実際にサーバーデータを求めているかを知るまでは、その呼び出しを待つべきではありません。
たとえば、以下の/a/b/c
ルートを考えてみましょう。
ユーザーが/ -> /a/b/c
にナビゲートする場合、a
とb
のサーバーローダーとc
のclientLoader
を実行する必要があります。これは最終的に(またはそうでなければ)独自のサーバーloader
を呼び出す可能性があります。c
のサーバーloader
をa
/b
の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で消費されることを目的としている場合は、より複雑な構造(Date
やPromise
インスタンスなど)をストリーミングダウンできるように、turbo-stream
エンコーディングを活用したいと考えています。しかし、外部からアクセスされる場合は、より簡単に消費できる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へのアップグレードの準備が整います。
注: 特定のResponse
インスタンスを返す必要がある外部からアクセスされるリソースルートにdefineLoader
/defineAction
を使用することはお勧めしません。これらの場合は、loader
/LoaderFunctionArgs
を使用することをお勧めします。
レスポンススタブとリソースルート
上記のように、headers
エクスポートは廃止され、代わりにloader
とaction
関数に渡される新しいresponse
スタブが使用されます。これは、リソースルートではやや混乱する可能性がありますが、これは、実際には"スタブ"の概念が必要ないためです。複数のローダーの結果を単一のレスポンスにマージする必要がないからです。
一貫性を保つために、リソースルートのloader
/action
関数には引き続きresponse
スタブが渡されます。必要に応じて使用できます(非リソースルートハンドラー間でコードを共有する場合など)。
response
スタブと、カスタムステータス/ヘッダーを持つResponse
を返すのを避けるのが最善ですが、そうした場合、以下のロジックが適用されます。
Response
インスタンスのステータスは、response
スタブのステータスよりも優先されます。response
スタブのheaders
に対するヘッダー操作は、返されたResponse
のヘッダーインスタンスに再適用されます。
その他の詳細
ストリーミングデータ形式
以前は、RemixはJSON.stringify
を使用してローダー/アクションのデータをワイヤー上でシリアル化していました。また、defer
レスポンスをサポートするためにカスタムのストリーミング形式を実装する必要がありました。
シングルフェッチでは、Remixは内部でturbo-stream
を使用するようになりました。これは、ストリーミングをファーストクラスでサポートし、JSONよりも複雑なデータを自動的にシリアル化/逆シリアル化することができます。BigInt
、Date
、Error
、Map
、Promise
、RegExp
、Set
、Symbol
、URL
などのデータ型は、turbo-stream
を介して直接ストリーミングダウンできます。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
処理をキャンセルし、保留中のプロミスを自動的に拒否し、それらのエラーをクライアントにストリーミングアップすることができます。デフォルトでは、これは4950ミリ秒後に発生します。これは、ほとんどのentry.server.tsxファイルの現在の5000ミリ秒のABORT_DELAY
よりわずかに小さい値で、プロミスをキャンセルし、React側の処理を中止する前に、拒否をReactレンダラーを介してストリーミングアップする必要があるためです。
これは、entry.server.tsx
からstreamTimeout
という数値をエクスポートすることで制御できます。Remixはこの値を、loader
/action
の保留中のプロミスを拒否するミリ秒数として使用します。この値は、Reactレンダラーを中止するタイムアウトとは切り離しておくことをお勧めします。また、Reactのタイムアウトを常に高い値に設定しておく必要があります。これにより、streamTimeout
から基礎となる拒否をストリーミングダウンする時間があります。
再検証
以前は、Remixはすべてのaction
送信後に、アクティブなすべてのローダーを再検証していました。これは、action
の結果に関係なく行われていました。shouldRevalidate
を使用して、ルートごとに再検証をオプトアウトすることができました。
シングルフェッチでは、action
が4xx/5xx
ステータスコードを持つResponse
を返し、またはスローした場合、Remixはデフォルトではローダーを再検証しません。action
が4xx/5xx
レスポンスではないものを返し、またはスローした場合、再検証の動作は変更されません。ここでの理由は、ほとんどの場合、4xx
/5xx
レスポンスを返す場合、実際にはデータを変更していないため、データを再読み込みする必要がないからです。
4xx/5xx
アクションレスポンス後に、1つ以上のローダーを再検証する必要がある場合は、shouldRevalidate
関数からtrue
を返すことで、ルートごとに再検証をオプトインすることができます。また、action
のステータスコードに基づいて決定する必要がある場合は、関数に渡される新しいunstable_actionStatus
パラメーターもあります。
再検証は、シングルフェッチのHTTP呼び出しの?_routes
クエリ文字列パラメーターを介して処理され、呼び出されるローダーが制限されます。つまり、細かい再検証を行う場合、リクエストされているルートに基づいてキャッシュ列挙が行われます。ただし、すべての情報はURLに含まれているため、特別なCDN構成は不要です(これはカスタムヘッダーを介して行われた場合とは異なり、CDNでVary
ヘッダーを尊重する必要がありました)。