シングルフェッチ

これは不安定なAPIであり、今後も変更され続ける可能性があります。本番環境では採用しないでください。

シングルフェッチは、新しいデータのデータ読み込み戦略とストリーミング形式です。シングルフェッチを有効にすると、Remixはクライアント側の遷移時にサーバーに対して単一のHTTP呼び出しを行うようになり、複数のHTTP呼び出しを並行して行う(ローダーごとに1回)ことはありません。さらに、シングルフェッチでは、DateErrorPromiseRegExpなど、loaderactionから裸のオブジェクトをダウンストリーム送信することもできます。

概要

Remixは、v2.9.0future.unstable_singleFetchフラグで、"シングルフェッチ"(RFC)のサポートを導入しました。これにより、この動作を選択できます。シングルフェッチは、React Router v7でデフォルトになります。

シングルフェッチを有効にすることで、初期段階での労力を抑え、時間をかけて段階的にすべての破壊的変更を採用することができます。シングルフェッチを有効にするために必要な最小限の変更を適用し、移行ガイドを使用してアプリケーションの段階的な変更を行うことで、React Router v7へのスムーズで破壊的でないアップグレードを保証できます。

また、破壊的変更も確認してください。特にシリアル化とステータス/ヘッダーの動作に関する、いくつかの根本的な動作変更について理解しておく必要があります。

シングルフェッチの有効化

1. 未来のフラグを有効にする

vite.config.ts
export default defineConfig({
  plugins: [
    remix({
      future: {
        // ...
        unstable_singleFetch: true,
      },
    }),
    // ...
  ],
});

2. 廃止されたfetchポリフィル

シングルフェッチでは、undicifetchポリフィルとして使用するか、Node 20+の組み込みのfetchを使用する必要があります。これは、@remix-run/web-fetchポリフィルには存在しない、そこに利用可能なAPIに依存しているためです。詳細は、以下の2.9.0リリースノートのUndiciセクションを参照してください。

  • Node 20+を使用している場合は、installGlobals()への呼び出しを削除し、Nodeの組み込みのfetch(これはundiciと同じです)を使用します。

  • 独自サーバーを管理し、installGlobals()を呼び出している場合は、undiciを使用するためにinstallGlobals({ nativeFetch: true })を呼び出す必要があります。

    - installGlobals();
    + 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.tsxrenderToPipeableStreamまたはrenderToReadableStreamなどのReactストリーミングレンダラーAPIを使用する必要があります。

これは、HTTPレスポンスをストリーミングする必要があるという意味ではありません。renderToPipeableStreamonAllReadyオプションまたはrenderToReadableStreamallReadyプロミスを活用することで、引き続き一度に完全なドキュメントを送信することができます。

クライアント側では、ストリーミングされたデータはSuspense境界でラップされて配信されるため、クライアント側のhydrateRoot呼び出しをstartTransition呼び出しでラップする必要があることも意味します。

破壊的変更

シングルフェッチには、いくつかの破壊的変更が導入されています。一部はフラグを有効にしたときにすぐに処理する必要があるもの、一部はフラグを有効にした後に段階的に処理できるものです。次のメジャーバージョンにアップグレードする前に、これらのすべてが処理されていることを確認する必要があります。

すぐに処理する必要がある変更:

  • 廃止されたfetchポリフィル: 古いinstallGlobals()ポリフィルはシングルフェッチでは機能しません。undiciベースのポリフィルを取得するには、ネイティブのNode 20のfetchAPIを使用するか、カスタムサーバーでinstallGlobals({ nativeFetch: true })を呼び出す必要があります。
  • 廃止されたheadersエクスポート: headers関数は、シングルフェッチが有効になっている場合は使用されなくなり、代わりにloader / action関数に渡される新しいresponseスタブが使用されます。

時間をかけて処理する必要があることを理解しておくべき変更:

  • 新しいストリーミングデータ形式: シングルフェッチは、turbo-streamを介して新しいストリーミング形式を内部で使用します。これは、JSONよりも複雑なデータをストリーミングできることを意味します。
  • 自動シリアル化なし: loaderaction関数から返された裸のオブジェクトは、もはや自動的にJSONResponseに変換されず、ワイヤー上でそのままシリアル化されます。
  • タイプ推論の更新: 最も正確なタイプ推論を得るためには、次の2つのことを行う必要があります。
    • tsconfig.jsoncompilerOptions.types配列の最後に@remix-run/react/future/single-fetch.d.tsを追加します。
    • ルートでunstable_defineLoader/unstable_defineActionを使用し始めます。
      • これは段階的に行うことができます。現在の状態では、ほとんどの場合、正確なタイプ推論が得られます。
  • オプトインのaction再検証: action4xx/5xx Response後の再検証は、オプトアウトではなく、オプトインになりました。

シングルフェッチを使用した新しいルートの追加

シングルフェッチが有効になっている場合、より強力なストリーミング形式とresponseスタブを活用するルートを作成することができます。

適切なタイプ推論を得るためには、最初にtsconfig.jsoncompilerOptions.types配列の最後に@remix-run/react/future/single-fetch.d.tsを追加する必要があります。これについては、タイプ推論セクションで詳しく説明します。

シングルフェッチでは、BigIntDateErrorMapPromiseRegExpSetSymbolURLなどのデータ型をローダーから返すことができます。

// routes/blog.$slug.tsx
import { unstable_defineLoader as defineLoader } from "@remix-run/node";
 
export const loader = defineLoader(
  async ({ params, response }) => {
    const { slug } = params;
 
    const comments = fetchComments(slug);
    const blogData = await fetchBlogData(slug);
 
    response.headers.set("Cache-Control", "max-age=300");
 
    return {
      content: blogData.content, // <- string
      published: blogData.date, // <- Date
      comments, // <- Promise
    };
  }
);
 
export default function BlogPost() {
  const blogData = useLoaderData<typeof loader>();
  //    ^? { content: string, published: Date, comments: Promise }
 
  return (
    <>
      <Header published={blogData.date} />
      <BlogContent content={blogData.content} />
      <Suspense fallback={<CommentsSkeleton />}>
        <Await resolve={blogData.comments}>
          {(comments) => (
            <BlogComments comments={comments} />
          )}
        </Await>
      </Suspense>
    </>
  );
}

シングルフェッチを使用したルートの移行

現在ローダーからResponseインスタンス(つまり、json/defer)を返している場合は、シングルフェッチの利点を活用するためにアプリケーションコードを変更する必要はありません。

ただし、将来的にReact Router v7にアップグレードする準備として、ルートごとに以下の変更を行うことをお勧めします。これは、ヘッダーとデータ型を更新しても何も壊れないことを検証する最も簡単な方法です。

型推論

シングルフェッチなしでは、loaderまたはactionから返されたプレーンなJavaScriptオブジェクトは自動的にJSONレスポンスにシリアル化されます(jsonを介して返された場合と同じです)。タイプ推論では、これが事実であると想定され、裸のオブジェクトの返却値は、JSONシリアル化された場合と同じように推論されます。

シングルフェッチでは、裸のオブジェクトは直接ストリーミングされるため、シングルフェッチを選択すると、組み込みのタイプ推論はもはや正確ではありません。たとえば、Dateはクライアント側で文字列にシリアル化されると想定されます😕。

シングルフェッチを使用するときに適切なタイプを取得するには、tsconfig.jsoncompilerOptions.types配列に含めることができる、一連のタイプオーバーライドを用意しました。これにより、タイプがシングルフェッチの動作と一致するようになります。

{
  "compilerOptions": {
    //...
    "types": [
      // ...
      "@remix-run/react/future/single-fetch.d.ts"
    ]
  }
}

🚨 シングルフェッチのタイプは、types内の他のRemixパッケージの後にあることを確認してください。これにより、既存のタイプをオーバーライドできます。

ローダー/アクション定義ユーティリティ

シングルフェッチを使用してローダーとアクションを定義する場合、タイプセーフティを高めるために、新しいunstable_defineLoaderunstable_defineActionユーティリティを使用できます。

import { unstable_defineLoader as defineLoader } from "@remix-run/node";
 
export const loader = defineLoader(({ request }) => {
  //                                  ^? Request
});

これにより、引数のタイプが得られるだけでなく(LoaderFunctionArgsは廃止されます)、シングルフェッチと互換性のあるタイプを返していることも保証されます。

export const loader = defineLoader(() => {
  return { hello: "world", badData: () => 1 };
  //                       ^^^^^^^ タイプエラー: `badData`はシリアル化できません
});
 
export const action = defineAction(() => {
  return { hello: "world", badData: new CustomType() };
  //                       ^^^^^^^ タイプエラー: `badData`はシリアル化できません
});

シングルフェッチは、以下の返却タイプをサポートしています。

type Serializable =
  | undefined
  | null
  | boolean
  | string
  | symbol
  | number
  | bigint
  | Date
  | URL
  | RegExp
  | Error
  | Array<Serializable>
  | { [key: PropertyKey]: Serializable } // シリアル化可能な値を持つオブジェクト
  | Map<Serializable, Serializable>
  | Set<Serializable>
  | Promise<Serializable>;

defineClientLoader/defineClientActionというクライアント側の同等のユーティリティもあります。これらは、clientLoader/clientActionから返されるデータはワイヤー上でシリアル化する必要がないため、同じ返却値の制限はありません。

import { unstable_defineLoader as defineLoader } from "@remix-run/node";
import { unstable_defineClientLoader as defineClientLoader } from "@remix-run/react";
 
export const loader = defineLoader(() => {
  return { msg: "Hello!", date: new Date() };
});
 
export const clientLoader = defineClientLoader(
  async ({ serverLoader }) => {
    const data = await serverLoader<typeof loader>();
    //    ^? { msg: string, date: Date }
    return {
      ...data,
      client: "World!",
    };
  }
);
 
export default function Component() {
  const data = useLoaderData<typeof clientLoader>();
  //    ^? { msg: string, date: Date, client: string }
}

これらのユーティリティは、主にuseLoaderDataとその同等のユーティリティに対するタイプ推論のためです。特定のResponseを返し、Remix API(useFetcherなど)では使用されないリソースルートがある場合は、通常のloader/action定義のままにすることができます。これらのルートをdefineLoader/defineActionを使用して変換すると、turbo-streamResponseインスタンスをシリアル化できないため、タイプエラーが発生します。

useLoaderDatauseActionDatauseRouteLoaderDatauseFetcher

これらのメソッドは、コードの変更を必要としません。シングルフェッチのタイプを追加すると、ジェネリックが正しく逆シリアル化されます。

export const loader = defineLoader(async () => {
  const data = await fetchSomeData();
  return {
    message: data.message, // <- string
    date: data.date, // <- Date
  };
});
 
export default function Component() {
  // ❌ シングルフェッチの前は、タイプはJSON.stringifyを介してシリアル化されていました。
  const data = useLoaderData<typeof loader>();
  //    ^? { message: string, date: string }
 
  // ✅ シングルフェッチでは、タイプはturbo-streamを介してシリアル化されます。
  const data = useLoaderData<typeof loader>();
  //    ^? { message: string, date: Date }
}

useMatches

useMatchesでは、手動でキャストしてローダータイプを指定する必要があります。これにより、match.dataに対する適切なタイプ推論が得られます。シングルフェッチを使用する場合は、UIMatchタイプをUIMatch_SingleFetchに置き換える必要があります。

  let matches = useMatches();
- let rootMatch = matches[0] as UIMatch<typeof loader>;
+ let rootMatch = matches[0] as UIMatch_SingleFetch<typeof loader>;

meta関数

meta関数も、現在のルートローダーと祖先ルートローダーのタイプを示すジェネリックを必要とし、これによりdatamatchesパラメーターが正しく型付けされます。シングルフェッチを使用する場合は、MetaArgsタイプをMetaArgs_SingleFetchに置き換える必要があります。

  export function meta({
    data,
    matches,
- }: MetaArgs<typeof loader, { root: typeof rootLoader }>) {
+ }: MetaArgs_SingleFetch<typeof loader, { root: typeof rootLoader }>) {
    // ...
  }

ヘッダー

headers関数は、シングルフェッチが有効になっている場合は使用されなくなります。 代わりに、loader / action関数には、その実行に固有の変更可能なResponseStubが渡されます。

  • HTTPレスポンスのステータスを変更するには、statusフィールドを直接設定します。
    • response.status = 201
  • HTTPレスポンスのヘッダーを設定するには、標準のHeadersAPIを使用します。
    • response.headers.set(name, value)
    • response.headers.append(name, value)
    • response.headers.delete(name)
export const action = defineAction(
  async ({ request, response }) => {
    if (!loggedIn(request)) {
      response.status = 401;
      response.headers.append("Set-Cookie", "foo=bar");
      return { message: "Invalid Submission!" };
    }
    await addItemToDb(request);
    return null;
  }
);

これらのレスポンススタブをスローして、ローダーとアクションのフローを短絡させることもできます。

export const loader = defineLoader(
  ({ request, response }) => {
    if (shouldRedirectToHome(request)) {
      response.status = 302;
      response.headers.set("Location", "/");
      throw response;
    }
    // ...
  }
);

loader/actionには、それぞれ固有のresponseインスタンスが渡されるため、他のloader/action関数で設定された内容を確認することはできません(これは競合状態が発生する可能性があります)。結果として得られるHTTPレスポンスのステータスとヘッダーは、以下のように決定されます。

  • ステータスコード
    • すべてのステータスコードが設定されていないか、値が<300の場合は、最も深いステータスコードがHTTPレスポンスに使用されます。
    • すべてのステータスコードが設定されているか、値が>=300の場合は、最も浅い>=300の値がHTTPレスポンスに使用されます。
  • ヘッダー
    • Remixはヘッダー操作を追跡し、すべてのハンドラーが完了した後に新しいHeadersインスタンスでそれらを再生します。
    • これらは、順序どおりに、最初にアクション(存在する場合)、次に上から下にローダーの順で再生されます。
    • 子ハンドラーのheaders.setは、親ハンドラーの値を上書きします。
    • headers.appendを使用すると、親ハンドラーと子ハンドラーの両方から同じヘッダーを設定できます。
    • headers.deleteを使用すると、親ハンドラーで設定された値を削除できますが、子ハンドラーで設定された値を削除することはできません。

シングルフェッチは裸のオブジェクトの返却をサポートしており、ステータス/ヘッダーを設定するために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ルートを考えてみましょう。

// routes/a.tsx
export function loader() {
  return { data: "A" };
}
 
// routes/a.b.tsx
export function loader() {
  return { data: "B" };
}
 
// routes/a.b.c.tsx
export function loader() {
  return { data: "C" };
}
 
export function clientLoader({ serverLoader }) {
  await doSomeStuff();
  const data = await serverLoader();
  return { data };
}

ユーザーが/ -> /a/b/cにナビゲートする場合、abのサーバーローダーとcclientLoaderを実行する必要があります。これは最終的に(またはそうでなければ)独自のサーバーloaderを呼び出す可能性があります。cのサーバーloadera/bloaderを取得するためのシングルフェッチ呼び出しに含めるかどうかを決定することはできませんし、ウォーターフォールを導入せずにcが実際にserverLoader呼び出しを行ったり(または返したり)するまで遅らせることもできません。

したがって、clientLoaderをエクスポートするルートは、シングルフェッチをオプトアウトし、serverLoaderを呼び出すと、そのルートのサーバーloaderのみを取得するための単一のフェッチが行われます。clientLoaderをエクスポートしないすべてのルートは、単一のHTTPリクエストでフェッチされます。

したがって、上記のルート設定で/ -> /a/b/cにナビゲートすると、最初にabのルートに対して単一のシングルフェッチ呼び出しが行われます。

GET /a/b/c.data?_routes=routes/a,routes/b

そして、cserverLoaderを呼び出すと、cのサーバーloaderだけを取得するための独自の呼び出しが行われます。

GET /a/b/c.data?_routes=routes/c

リソースルート

シングルフェッチで使用される新しいストリーミング形式により、loaderaction関数から返される生のJavaScriptオブジェクトは、もはやjson()ユーティリティを介して自動的にResponseインスタンスに変換されなくなりました。代わりに、ナビゲーションデータの読み込みでは、他のローダーデータと結合され、turbo-streamレスポンスでダウンストリーム送信されます。

これは、個別にヒットすることを目的としたリソースルートにとっては興味深いジレンマです。これは、常にRemix APIを介するとは限りません。また、他のHTTPクライアント(fetchcURLなど)からもアクセスできます。

リソースルートが内部のRemix APIで消費されることを目的としている場合は、より複雑な構造(DatePromiseインスタンスなど)をストリーミングダウンできるように、turbo-streamエンコーディングを活用したいと考えています。しかし、外部からアクセスされる場合は、より簡単に消費できるJSON構造を返す方が良いでしょう。したがって、生のオブジェクトをv2で返す場合、動作は少し曖昧です。turbo-streamjson()のどちらでシリアル化する必要がありますか?

Remix v2では、後方互換性を維持し、シングルフェッチの未来のフラグの採用を容易にするために、Remix APIからアクセスされる場合と外部からアクセスされる場合で動作が異なります。将来的には、Remixでは、外部からアクセスされる場合に生のオブジェクトをストリーミングダウンしたくない場合は、独自のJSONレスポンスを返す必要があります。

シングルフェッチが有効になっている場合のRemix v2の動作は次のとおりです。

  • useFetcherなどのRemix APIからアクセスする場合、生のJavaScriptオブジェクトは、通常のローダーとアクションと同様にturbo-streamレスポンスとして返されます(これは、useFetcherがリクエストに.dataサフィックスを追加するためです)。

  • fetchcURLなどの外部ツールからアクセスする場合、v2の後方互換性を維持するために、引き続き自動的にjson()に変換されます。

    • Remixはこの状況が発生すると、廃止予定の警告をログに記録します。
    • 必要に応じて、影響を受けるリソースルートハンドラーを更新して、Responseオブジェクトを返すことができます。
    • これらの廃止予定の警告に対処することで、今後のRemix v3へのアップグレードの準備が整います。
    app/routes/resource.tsx
    export function loader() {
      return {
        message: "My externally-accessed resource route",
      };
    }
    app/routes/resource.tsx
    export function loader() {
      return Response.json({
        message: "My externally-accessed resource route",
      });
    }

注: 特定のResponseインスタンスを返す必要がある外部からアクセスされるリソースルートにdefineLoader/defineActionを使用することはお勧めしません。これらの場合は、loader/LoaderFunctionArgsを使用することをお勧めします。

レスポンススタブとリソースルート

上記のように、headersエクスポートは廃止され、代わりにloaderaction関数に渡される新しいresponseスタブが使用されます。これは、リソースルートではやや混乱する可能性がありますが、これは、実際には"スタブ"の概念が必要ないためです。複数のローダーの結果を単一のレスポンスにマージする必要がないからです。

app/routes/resource.tsx
// 独自のリソースを使用するのが最も簡単な方法です。
export async function loader() {
  const data = await getData();
  return Response.json(data, {
    status: 200,
    headers: {
      "X-Custom": "whatever",
    },
  });
}

一貫性を保つために、リソースルートのloader/action関数には引き続きresponseスタブが渡されます。必要に応じて使用できます(非リソースルートハンドラー間でコードを共有する場合など)。

app/routes/resource.tsx
// しかし、responseスタブで値を設定することもできます。
export async function loader({
  response,
}: LoaderFunctionArgs) {
  const data = await getData();
  response?.status = 200;
  response?.headers.set("X-Custom", "whatever");
  return Response.json(data);
}

responseスタブと、カスタムステータス/ヘッダーを持つResponseを返すのを避けるのが最善ですが、そうした場合、以下のロジックが適用されます。

  • Responseインスタンスのステータスは、responseスタブのステータスよりも優先されます。
  • responseスタブのheadersに対するヘッダー操作は、返されたResponseのヘッダーインスタンスに再適用されます。

その他の詳細

ストリーミングデータ形式

以前は、RemixはJSON.stringifyを使用してローダー/アクションのデータをワイヤー上でシリアル化していました。また、deferレスポンスをサポートするためにカスタムのストリーミング形式を実装する必要がありました。

シングルフェッチでは、Remixは内部でturbo-streamを使用するようになりました。これは、ストリーミングをファーストクラスでサポートし、JSONよりも複雑なデータを自動的にシリアル化/逆シリアル化することができます。BigIntDateErrorMapPromiseRegExpSetSymbolURLなどのデータ型は、turbo-streamを介して直接ストリーミングダウンできます。Errorのサブタイプも、クライアントにグローバルに利用可能なコンストラクターがある場合はサポートされます(SyntaxErrorTypeErrorなど)。

シングルフェッチを有効にしたときにコードを変更する必要があるかどうかは、以下のとおりです。

  • 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から基礎となる拒否をストリーミングダウンする時間があります。

app/entry.server.tsx
// ハンドラー関数の保留中のすべてのプロミスを5秒後に拒否します。
export const streamTimeout = 5000;
 
// ...
 
function handleBrowserRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return new Promise((resolve, reject) => {
    const { pipe, abort } = renderToPipeableStream(
      <RemixServer
        context={remixContext}
        url={request.url}
        abortDelay={ABORT_DELAY}
      />,
      {
        onShellReady() {
          /* ... */
        },
        onShellError(error: unknown) {
          /* ... */
        },
        onError(error: unknown) {
          /* ... */
        },
      }
    );
 
    // Reactレンダラーを10秒後に自動的にタイムアウトします。
    setTimeout(abort, 10000);
  });
}

再検証

以前は、Remixはすべてのaction送信後に、アクティブなすべてのローダーを再検証していました。これは、actionの結果に関係なく行われていました。shouldRevalidateを使用して、ルートごとに再検証をオプトアウトすることができました。

シングルフェッチでは、action4xx/5xxステータスコードを持つResponseを返し、またはスローした場合、Remixはデフォルトではローダーを再検証しません。action4xx/5xxレスポンスではないものを返し、またはスローした場合、再検証の動作は変更されません。ここでの理由は、ほとんどの場合、4xx/5xxレスポンスを返す場合、実際にはデータを変更していないため、データを再読み込みする必要がないからです。

4xx/5xxアクションレスポンス後に、1つ以上のローダーを再検証する必要がある場合は、shouldRevalidate関数からtrueを返すことで、ルートごとに再検証をオプトインすることができます。また、actionのステータスコードに基づいて決定する必要がある場合は、関数に渡される新しいunstable_actionStatusパラメーターもあります。

再検証は、シングルフェッチのHTTP呼び出しの?_routesクエリ文字列パラメーターを介して処理され、呼び出されるローダーが制限されます。つまり、細かい再検証を行う場合、リクエストされているルートに基づいてキャッシュ列挙が行われます。ただし、すべての情報はURLに含まれているため、特別なCDN構成は不要です(これはカスタムヘッダーを介して行われた場合とは異なり、CDNでVaryヘッダーを尊重する必要がありました)。