シングルフェッチ

シングルフェッチは、新しいデータローディング戦略とストリーミングフォーマットです。 シングルフェッチを有効にすると、Remixはクライアント側の遷移でサーバーに対して単一のHTTP呼び出しを行うようになり、複数のHTTP呼び出しを並行して(ローダーごとに1つずつ)行うことはなくなります。 さらに、シングルフェッチでは、DateErrorPromiseRegExpなど、loaderactionから生のオブジェクトを送信することもできます。

概要

Remixは、v2.9.0(後にv2.13.0future.v3_singleFetchとして安定化)のfuture.unstable_singleFetchフラグの後ろに「シングルフェッチ」(RFC)のサポートを追加しました。 これにより、この動作を選択できます。 シングルフェッチはReact Router v7でデフォルトになります。

シングルフェッチの有効化は、当初は低労力で、その後は時間をかけてすべての破壊的な変更を段階的に導入することを目的としています。 最初はシングルフェッチを有効にするために必要な最小限の変更を適用し、その後は移行ガイドを使用してアプリケーションの段階的な変更を行い、React Router v7へのスムーズで非破壊的なアップグレードを確実にします。

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

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

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

vite.config.ts
export default defineConfig({
  plugins: [
    remix({
      future: {
        // ...
        v3_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を使用します。

  • 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.tsxrenderToPipeableStream/renderToReadableStreamへのoptions.nonceパラメーターに。 Remixのストリーミングドキュメントも参照してください。

5. renderToStringを置き換える(使用している場合)

ほとんどのRemixアプリではrenderToStringは使用していませんが、entry.server.tsxで使用することを選択している場合は、以下を読み進めてください。 そうでない場合は、この手順をスキップできます。

ドキュメントリクエストとデータリクエストの整合性を維持するために、turbo-streamは初期ドキュメントリクエストでのデータ送信にもフォーマットとして使用されます。 つまり、シングルフェッチを選択すると、アプリケーションではrenderToStringを使用できなくなり、entry.server.tsxrenderToPipeableStreamrenderToReadableStreamなどのReactストリーミングレンダラーAPIを使用する必要があります。

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

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

破壊的な変更

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

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

  • 廃止されたfetchポリフィル: 古いinstallGlobals()ポリフィルはシングルフェッチでは機能しません。 Node 20のネイティブfetchAPIを使用するか、カスタムサーバーでinstallGlobals({ nativeFetch: true })を呼び出して、undiciベースのポリフィルを取得する必要があります。
  • headersエクスポートがデータリクエストに適用される: headers関数は、ドキュメントリクエストとデータリクエストの両方に適用されるようになりました。

時間をかけて処理する必要がある変更:

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

シングルフェッチを有効にすると、より強力なストリーミングフォーマットを活用したルートを作成できます。

適切なタイプ推論を得るには、v3_singleFetch: trueでRemixのFutureインターフェースを拡張する必要があります。 詳細は、タイプ推論セクションをご覧ください。

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

// routes/blog.$slug.tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
 
export async function loader({
  params,
}: LoaderFunctionArgs) {
  const { slug } = params;
 
  const comments = fetchComments(slug);
  const blogData = await fetchBlogData(slug);
 
  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がクライアント側で文字列にシリアル化されると仮定されます 😕。

シングルフェッチのタイプを有効にする

シングルフェッチのタイプに切り替えるには、v3_singleFetch: trueでRemixのFutureインターフェースを拡張する必要があります。 これは、tsconfig.json > includeでカバーされている任意のファイルで行うことができます。 Remixプラグインのfuture.v3_singleFetch未来フラグと併せて、vite.config.tsで行うことをお勧めします。

declare module "@remix-run/server-runtime" {
  // またはcloudflare、denoなど
  interface Future {
    v3_singleFetch: true;
  }
}

これで、useLoaderDatauseActionData、およびtypeof loaderジェネリックを使用するその他のユーティリティは、シングルフェッチのタイプを使用するようになりました。

import { useLoaderData } from "@remix-run/react";
 
export function loader() {
  return {
    planet: "world",
    date: new Date(),
  };
}
 
export default function Component() {
  const data = useLoaderData<typeof loader>();
  //    ^? { planet: string, date: Date }
}

関数とクラスインスタンス

一般的に、関数はネットワークを介して確実に送信することはできないため、undefinedとしてシリアル化されます。

import { useLoaderData } from "@remix-run/react";
 
export function loader() {
  return {
    planet: "world",
    date: new Date(),
    notSoRandom: () => 7,
  };
}
 
export default function Component() {
  const data = useLoaderData<typeof loader>();
  //    ^? { planet: string, date: Date, notSoRandom: undefined }
}

メソッドもシリアル化できないため、クラスインスタンスはシリアル化可能なプロパティに絞られます。

import { useLoaderData } from "@remix-run/react";
 
class Dog {
  name: string;
  age: number;
 
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
 
  bark() {
    console.log("woof");
  }
}
 
export function loader() {
  return {
    planet: "world",
    date: new Date(),
    spot: new Dog("Spot", 3),
  };
}
 
export default function Component() {
  const data = useLoaderData<typeof loader>();
  //    ^? { planet: string, date: Date, spot: { name: string, age: number, bark: undefined } }
}

clientLoaderclientAction

clientLoaderの引数とclientActionの引数の型を含めてください。 これは、クライアントデータ関数を検出する方法です。

クライアント側のローダーとアクションからのデータはシリアル化されないため、これらの型は保持されます。

import {
  useLoaderData,
  type ClientLoaderFunctionArgs,
} from "@remix-run/react";
 
class Dog {
  /* ... */
}
 
// 引数の型を注釈する! 👇
export function clientLoader(_: ClientLoaderFunctionArgs) {
  return {
    planet: "world",
    date: new Date(),
    notSoRandom: () => 7,
    spot: new Dog("Spot", 3),
  };
}
 
export default function Component() {
  const data = useLoaderData<typeof clientLoader>();
  //    ^? { planet: string, date: Date, notSoRandom: () => number, spot: Dog }
}

ヘッダー

headers関数は、シングルフェッチが有効になっている場合、ドキュメントリクエストとデータリクエストの両方で使用されます。 この関数を使用して、並行して実行されるローダーから返されたヘッダーをマージするか、特定のactionHeadersを返します。

返されるレスポンス

シングルフェッチでは、Responseインスタンスを返す必要がなくなり、生のオブジェクトを直接返すことができます。 したがって、json/deferユーティリティは、シングルフェッチを使用する場合は廃止されたものとして扱う必要があります。 これらは、v2の間は残るので、すぐに削除する必要はありません。 次のメジャーバージョンでは削除される可能性があるため、それまでに段階的に削除することをお勧めします。

v2では、引き続き通常のResponseインスタンスを返し続け、status/headersはドキュメントリクエストと同様に(headers()関数を介してヘッダーをマージして)有効になります。

時間をかけて、ローダーとアクションから返されるレスポンスを排除する必要があります。

  • loader/actionstatus/headersを設定せずにjson/deferを返していた場合、json/deferへの呼び出しを削除して、データを直接返すことができます。
  • loader/actionjson/deferを介してカスタムstatus/headersを返していた場合は、新しいdata()ユーティリティを使用するように切り替える必要があります。

クライアントローダー

アプリにclientLoader関数を使用するルートがある場合、シングルフェッチの動作がわずかに変化することに注意することが重要です。 clientLoaderは、サーバーのloader関数の呼び出しをオプトアウトするための方法として意図されているため、シングルフェッチの呼び出しがそのサーバーの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を呼び出します。 a/bloaderを取得したいときに、cのサーバーのloaderをシングルフェッチ呼び出しに含めることはできませんし、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で使用されることを目的としている場合、turbo-streamエンコーディングを活用して、DatePromiseインスタンスなどのより複雑な構造をストリーミングできるようになります。 しかし、外部からアクセスされた場合、JSON構造の方が使いやすく、消費しやすいでしょう。 したがって、v2で生のオブジェクトを返した場合、その動作は少し曖昧になります。 turbo-streamまたはjson()でシリアル化すべきでしょうか?

後方互換性を容易にし、シングルフェッチの未来フラグの導入を容易にするために、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",
      });
    }

詳細情報

ストリーミングデータフォーマット

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

シングルフェッチでは、Remixは現在、turbo-streamを内部で使用しています。 これは、ストリーミングをファーストクラスでサポートしており、JSONよりも複雑なデータを自動的にシリアル化/デシリアル化できます。 以下のデータ型は、turbo-streamを介して直接ストリーミングできます。 BigIntDateErrorMapPromiseRegExpSetSymbolURLErrorのサブタイプも、クライアント側でグローバルに利用可能なコンストラクターがある場合はサポートされます(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処理をキャンセルし、保留中のプロミスを自動的に拒否し、それらのエラーをクライアントにストリーミングできます。 デフォルトでは、これは4950ms後に発生します。 これは、現在のほとんどのentry.server.tsxファイルのABORT_DELAYである5000msより少し短い値です。 これは、Reactの処理を中止する前に、プロミスをキャンセルし、その拒否をReactレンダラーを介してストリーミングする必要があるためです。

これは、entry.server.tsxからstreamTimeoutという数値をエクスポートすることで制御できます。 Remixは、その値をミリ秒で、loader/actionの保留中のPromiseを拒否するまでの時間として使用します。 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);
  });
}

再検証

通常のナビゲーションの動作

シングルフェッチの利点は、シンプルな精神モデルとドキュメントリクエストとデータリクエストの整合性に加えて、よりシンプルで(うまくいけば)より良いキャッシュ動作です。 一般的に、シングルフェッチではHTTPリクエストの数が減り、以前の複数フェッチの動作と比較して、キャッシュされた結果が多くなることが期待されます。

キャッシュの断片化を減らすため、シングルフェッチではGETナビゲーションでのデフォルトの再検証動作が変更されています。 以前は、Remixは、shouldRevalidateを介してオプトインしない限り、再利用された祖先ルートのローダーを再実行しませんでした。 現在は、RemixはGET /a/b/c.dataのようなシングルフェッチリクエストの簡単なケースでは、デフォルトでこれらのローダーを再実行します。 shouldRevalidateclientLoader関数が存在しない場合、これがアプリの動作になります。

いずれかのアクティブなルートにshouldRevalidateまたはclientLoaderを追加すると、_routesパラメーターを含む、細かいシングルフェッチ呼び出しがトリガーされます。 このパラメーターは、実行するルートのサブセットを指定します。

clientLoaderが内部でserverLoader()を呼び出すと、その特定のルートに対して別々のHTTP呼び出しがトリガーされ、以前の動作と同じになります。

たとえば、/a/bにいて、/a/b/cにナビゲートする場合、次のようになります。

  • shouldRevalidateまたはclientLoader関数が存在しない場合: GET /a/b/c.data
  • すべてのルートにローダーがあるが、routes/ashouldRevalidateを介してオプトアウトしている場合:
    • GET /a/b/c.data?_routes=root,routes/b,routes/c
  • すべてのルートにローダーがあるが、routes/bclientLoaderがある場合:
    • GET /a/b/c.data?_routes=root,routes/a,routes/c
    • そして、BのclientLoaderserverLoader()を呼び出す場合:
      • GET /a/b/c.data?_routes=routes/b

この新しい動作がアプリケーションに適していない場合は、親ルートにshouldRevalidateを追加して、falseを返すことで、再検証しないという以前の動作にオプトインできます。 これにより、必要なシナリオでの再検証を防ぐことができます。

別のオプションは、高価な親ローダーの計算用にサーバー側のキャッシュを活用することです。

送信再検証の動作

以前は、Remixは、アクションの送信後、アクションの結果に関係なく、アクティブなすべてのローダーを再検証していました。 shouldRevalidateを介して、ルート単位で再検証をオプトアウトできました。

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

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

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