シングルフェッチ

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

概要

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

シングルフェッチの有効化は、初期段階では容易であり、その後、破壊的な変更を段階的に適用できます。シングルフェッチを有効にするために必要な最小限の変更を適用してから、移行ガイドを使用してアプリケーションの段階的な変更を行い、React Router v7へのスムーズな、破壊的ではないアップグレードを確保します。

また、シリアライゼーションとステータス/ヘッダーの動作に関するいくつかの基礎となる動作の変更について、破壊的変更も確認してください。

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

1. futureフラグを有効にする

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

2. 非推奨のfetchポリフィル

シングルフェッチでは、@remix-run/web-fetchポリフィルにはないAPIに依存しているため、fetchポリフィルとしてundiciを使用するか、Node 20以降の組み込みfetchを使用する必要があります。詳細については、以下の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の実装を調整する(必要に応じて)

シングルフェッチを有効にすると、複数のローダーを実行する必要がある場合でも、クライアント側のナビゲーションで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を使用できなくなり、renderToPipeableStreamまたはrenderToReadableStreamなどのReactストリーミングレンダラーAPIをentry.server.tsxで使用しなければなりません。

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

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

破壊的変更

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

すぐに対応する必要がある変更:

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

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

  • 新しいストリーミングデータ形式: シングルフェッチはturbo-streamを介して新しいストリーミング形式を内部的に使用するため、JSONよりも複雑なデータをストリーミングできます。
  • 自動シリアライゼーションなし: loaderaction関数から返された生のオブジェクトは、自動的にJSON Responseに変換されなくなり、ワイヤ上でそのままシリアライズされます。
  • 型推論の更新: 最も正確な型推論を得るには、v3_singleFetch: trueを使用してRemixのFutureインターフェースを拡張する必要があります。
  • GETナビゲーションでのオプトアウトへのデフォルトの再検証動作の変更: 通常のナビゲーションでのデフォルトの再検証動作はオプトインからオプトアウトに変更され、サーバーローダーはデフォルトで再実行されます。
  • オプトインaction再検証: action 4xx/5xx Response後の再検証は、オプトアウトではなくオプトインになります。

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

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

適切な型推論を得るには、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 futureフラグと共存させるために、vite.config.tsで行うことをお勧めします。

declare module "@remix-run/server-runtime" {
  // or cloudflare, deno, etc.
  interface Future {
    v3_singleFetch: true;
  }
}

これで、useLoaderDatauseActionDatatypeof 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 {
  /* ... */
}
 
// Make sure to annotate the types for the args! 👇
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()関数を使用してヘッダーをマージします)。

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

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

クライアントローダー

アプリに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のサーバーローダーと、独自のサーバーloaderを最終的に(またはしない可能性もある)呼び出す可能性のあるcclientLoaderを実行する必要があります。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によって消費されることを意図している場合、DatePromiseインスタンスなどのより複雑な構造をストリーミングできるようにするために、turbo-streamエンコーディングを利用できるようにしたいと考えています。ただし、外部からアクセスする場合、より簡単に消費できるJSON構造を返す方がおそらく望ましいでしょう。したがって、v2で生のオブジェクトを返す場合、動作はわずかに曖昧です。turbo-streamまたはjson()を介してシリアライズする必要がありますか?

後方互換性を容易にし、シングルフェッチfutureフラグの採用を容易にするために、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よりも複雑なデータを自動的にシリアライズ/デシリアライズできます。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処理をキャンセルし、保留中のプロミスを自動的に拒否し、それらのエラーをクライアントにストリーミングできます。デフォルトでは、これは4950ms後に発生します。これは、ほとんどのentry.server.tsxファイルの現在の5000ms ABORT_DELAYよりもわずかに短い値です。Reactレンダラーを中止する前に、プロミスをキャンセルして、拒否をReactレンダラーを介してストリーミングする必要があるためです。

これは、entry.server.tsxからstreamTimeout数値エクスポートをエクスポートすることで制御できます。Remixはこれを、loader/actionからの保留中のPromiseを拒否するミリ秒数として使用します。この値をReactレンダラーを中止するタイムアウトから切り離すことをお勧めします。基礎となる拒否をストリーミングする時間があるように、常にReactタイムアウトをより高い値に設定する必要があります。

app/entry.server.tsx
// Reject all pending promises from handler functions after 5 seconds
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) {
          /* ... */
        },
      }
    );
 
    // Automatically timeout the react renderer after 10 seconds
    setTimeout(abort, 10000);
  });
}

再検証

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

より単純なメンタルモデルとドキュメントリクエストとデータリクエストの整合性に加えて、シングルフェッチのもう1つの利点は、より単純な(そしておそらくより良い)キャッシング動作です。一般的に、シングルフェッチは、以前の複数フェッチ動作と比較して、HTTPリクエストの回数が少なくなり、結果がより頻繁にキャッシュされる可能性があります。

キャッシュの断片化を減らすために、シングルフェッチはGETナビゲーションでのデフォルトの再検証動作を変更します。以前は、shouldRevalidateを介してオプトインしない限り、Remixは再利用された祖先ルートのローダーを再実行しませんでした。現在、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/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

この新しい動作がアプリケーションにとって最適でない場合、必要なシナリオでfalseを返すshouldRevalidateを親ルートに追加することで、ローダーを再検証しないという古い動作にオプトバックインできます。

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

送信再検証動作

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

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

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

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