Remix 3におけるデータローディングの仕組み
Remix 3がデータ読み込みでloaderをコントローラーアクションに置き換える仕組みと、ルート、ストリーミングFrame、待機状態、エラー処理を解説します。
Remix 3では、GETルートを処理するコントローラーのアクション内でページデータを読み込みます。アクションは標準のWeb Responseを返し、通常はcontext.render()でこれを生成します。
Remix 2のアプリを本番リリースした経験がある方なら、まずloaderのエクスポートと、それを読み取るフックを探そうとするでしょう。しかし、新しいガイドにはそのどちらも登場しません。また、ローダーがRemix 3にも引き継がれたとする古い記事の内容は、Remix 3のドキュメントの記述と一致しません。
2026年10月1日にリリースされたRemix 3(v3.0.0)は、Remixの当初のローダーベースのアーキテクチャに関するこちらの解説で紹介されているRemix 1/2のローダー方式とは異なるモデルを採用しています。本記事では、データフェッチの実装場所、初回ロードとナビゲーション時のストリーミング、ペンディングUIの仕組み、そしてエラーの扱いについて解説します。
要点
- Remix 3では、GETルートに対応する
createController()のアクション内でページデータを取得し、そのデータをcontext.render()でレンダリングするコンポーネントにpropsとして渡します。 - ルートは
app/routes.tsで宣言し、コントローラーで実装して、router.map()で登録します。各コントローラーが担当するのは、自身のルートマップの直下のリーフのみです。 fallbackを持たないFrameは、そのルートが応答するまで初期HTMLの送信を保留します。fallbackを持つFrameは、まずフォールバックをストリーミングし、後から置き換えます。- レコードが存在しないといった想定内の失敗は、ステータス404または400の
Responseオブジェクトとして返すべきです。エラーをthrowするとrouter.fetch()がrejectされ、サーバーバウンダリーが500を返します。 - フォールバック送信後に遅延フレームが失敗した場合、エラーは報告され、フォールバックは画面に表示されたままになります。
Remix 3でローダーに代わるものは何か?
Remix 3では、かつてルートローダーが担っていた役割をコントローラーのアクションが果たします。ルートマップでURLとHTTPメソッドを宣言し、createController()で各ルートリーフに1つのアクションを紐付けます。アクションはcontext.paramsから型付きの値を読み取り、データをawaitして、Responseを返します。Routing and Controllersガイドではこのパターンが紹介されており、ローダーのエクスポートはどこにも登場しません。
// app/actions/albums/controller.tsx
import { createController } from "remix/router";
import { routes } from "../../routes.ts";
// getAlbum and getRecommendations are app data functions, not Remix APIs.
import { getAlbum, getRecommendations } from "./data.ts";
import { AlbumRecommendations } from "./recommendations.tsx";
import { AlbumPage } from "./show-page.tsx";
export default createController(routes.albums, {
actions: {
async show(context) {
// The second argument is an app-defined signal parameter.
let album = await getAlbum(context.params.albumId, context.request.signal);
if (!album) return new Response("Album not found", { status: 404 });
return context.render(<AlbumPage album={album} />);
},
async recommendations(context) {
let album = await getAlbum(context.params.albumId, context.request.signal);
if (!album) return new Response("Album not found", { status: 404 });
let recommendations = await getRecommendations(album);
return context.render(<AlbumRecommendations albums={recommendations} />);
},
},
});
Remix 2からの最大の変更点は、データフローの方向です。Remix 2では、コンポーネントがフックを通じてデータを取得(pull)していました。Remix 3では、アクションがpropsとしてデータをコンポーネントに渡します(push)。context.renderは、ルーターにrender()ミドルウェアがインストールされている場合にのみ使用できます。インストールされていなくても、アクションはプレーンなResponseやResponse.json()を返すことができます。
Remix 3ではデータローディングをどこに実装するのか?
Remix 3のアプリでは、app/routes.tsでURLとHTTPメソッドを宣言し、createController()でルートリーフごとに1つのアクションを実装し、router.map()で各コントローラーを登録します。
// app/routes.ts
import { get, route } from "remix/routes";
export const routes = route({
albums: route("/albums", {
show: get("/:albumId"),
recommendations: get("/:albumId/recommendations"),
}),
});
routes.albums.show.href({ albumId: "thriller" }); // "/albums/thriller"
各ルートのhref()ヘルパーは型付けされています。:albumIdはパス変数なので、albumIdは必須の引数になります。後でパターン名を変更した場合、古いキーを渡したままの呼び出しはすべて型チェックでエラーになります。
// app/router.ts
import { createRouter } from "remix/router";
import albumsController from "./actions/albums/controller.tsx";
import { render } from "./middleware/render.tsx"; // the app's own render middleware, built with renderWith()
import { routes } from "./routes.ts";
export const router = createRouter({
middleware: [render()],
});
router.map(routes.albums, albumsController);
コントローラーが担当するのは渡されたマップの直下のリーフのみであるため、ネストしたマップにはそれぞれrouter.map()の呼び出しが必要です。Remixはセットアップ時にすべてのマッピングを検証します。担当するリーフのいずれかに対応するアクションがないコントローラーがあると、リクエストを処理する前にセットアップ時点で例外がthrowされます。実際には、1ページにつき1つのGETルートを用意し、独立して読み込む領域ごとにもう1つずつ追加する設計を想定しておくとよいでしょう。
Remix 3における初回ロードとナビゲーションの仕組み
初回ロード時、Remix 3のレンダーミドルウェアは1つのHTMLドキュメントをストリーミングします。ナビゲーション時には、ブラウザーでrun()が開始された後、同一オリジンのリンクやフォームによって、ドキュメント全体を置き換える代わりに名前付きのFrame1つだけを再描画できます。ある領域に独自のリクエストが必要な場合、ページはその領域をFrameでレンダリングし、その領域用のGETルートを指定します。そのルートの背後にあるアクションは、アプリのドキュメントシェルを含めず、その領域のマークアップだけを返します。
// app/actions/albums/show-page.tsx
import { Frame } from "remix/component";
import type { Handle } from "remix/component";
import { routes } from "../../routes.ts";
import { Document } from "../document.tsx"; // app-defined shell component
import type { Album } from "./data.ts";
export function AlbumPage(handle: Handle<{ album: Album }>) {
return () => {
let { album } = handle.props;
return (
<Document title={album.title}>
<h1>{album.title}</h1>
<Frame
name="album-recommendations"
src={routes.albums.recommendations.href({ albumId: album.id })}
fallback={<p>Loading recommendations…</p>}
/>
</Document>
);
};
}
fallbackを持たないFrameはブロッキングです。Remixはフレームのルートが応答するまでHTMLの最初のチャンクを送信しないため、ページは完全な状態で届きます。fallbackを追加すると、フォールバックが初期HTMLに含まれて送信され、フレームルートのアクションが完了した時点でその出力に置き換えられます。その領域がなければページとして意味をなさない場合はブロッキングを、ページの他の部分を先に表示する価値がある場合はフォールバックを選択してください。
ブラウザーでrun()が開始された後は、同一オリジンのリンクやフォームによって、ドキュメント全体を遷移させる代わりに名前付きフレームを描画できます。これらの属性はStreaming UI with Framesに記載されています。
| 属性 | 効果 |
|---|---|
data-rmx-target | マウント済みの名前付きフレームを指定する |
data-rmx-src | フレームを描画するリクエストURLを設定する |
data-rmx-reset-scroll="false" | 現在のスクロール位置を維持する |
data-rmx-history="push|replace" | デフォルトの履歴の挙動を上書きする |
data-rmx-document | リンクやフォームをインターセプトの対象外にする |
リンクの場合、アドレスバーはhrefに遷移しますが、フレームに読み込まれる内容はdata-rmx-srcによって決まります。
<form
action={routes.albums.recommendations.href({ albumId: album.id })}
method="get"
data-rmx-target="album-recommendations"
>
<button type="submit">Refresh recommendations</button>
</form>
コンポーネント自身がフレームを再読み込みすることもできます。handle.frame.reload()はそのコンポーネントを含むフレームを再読み込みし、await handle.frames.get("album-recommendations")?.reload()はページ上の別の場所にある名前付きフレームを対象とします。その名前のフレームがマウントされていない場合、handle.frames.get()はundefinedを返します。フレームルートは信頼できるHTMLのみを返すべきなので、信頼できないコンテンツは事前にサーバー側でサニタイズしてください。
ペンディング状態:フォールバックとキャンセル
Remix 3では、FrameのフォールバックとリクエストのキャンセルによってペンディングUIを扱います。Frameのフォールバックが初期のローディングUIとなり、グローバルなナビゲーションのペンディング状態を取得するフックはガイドに記載されていません。
キャンセルはアプリ側で組み込む必要があります。run()に渡すブラウザー側のフレームリゾルバーはシグナルを受け取ります。ガイドによれば、このシグナルをfetch()に渡すことで、フレームが削除されたときや新しいリクエストに置き換えられたときにフレームリクエストが停止します。サーバー側では、レンダーミドルウェアがrequest.signalをrenderToStream()とそのフレームリクエストに渡します。キャンセルを自分のコードにまで伝搬させるには、データベースやfetchの呼び出しにもrequest.signalを渡してください。Errors and Cancellationによると、リクエストが中断されると、ルーターはrequest.signal.reasonでrejectします。
エラー状態:レスポンスとレポート用フック
Remix 3では、レコードが存在しない場合や入力が不正な場合は、ステータス404または400のResponseとして返します。エラーをthrowするとrouter.fetch()がrejectされ、サーバーバウンダリーが500で応答します。Remix 3にはすべてを捕捉するコンポーネントのエラーバウンダリーはなく、失敗の種類ごとにそれぞれの行き先があります。
| 失敗の種類 | 行き先 |
|---|---|
| 想定内の失敗(レコードが存在しない、不正な入力) | ステータス404または400のResponseを返す |
| アクションまたはミドルウェアでのthrow | router.fetch()がrejectされ、サーバーバウンダリーがエラーを報告して500を返す |
| ルートまたはブロッキングフレームのレンダリング失敗 | エラーが報告され、ストリームがrejectされる |
| フォールバック送信後の遅延フレームの失敗 | エラーが報告され、フォールバックはページに残る |
| ブラウザーランタイム(ハイドレーション、コンポーネント、スケジューラー、フレーム) | run()の戻り値に対するerrorイベント |
サーバーバウンダリーでは、remix/node-fetch-serverのcreateRequestListenerがonErrorオプションを受け付けます。ここで捕捉されなかったエラーをログに記録でき、必要に応じてデフォルトの500の代わりに送信するResponseを返すこともできます。レンダーミドルウェアのonErrorオプションとブラウザーのerrorイベントは、エラーを報告するだけです。フレームについては、失敗したレスポンスをどう扱うかをアプリ独自のリゾルバーが決定します。その領域に限定したHTMLにするか、前述のレポート用フックにエラーを送るかのどちらかです。ガイドに掲載されているリゾルバーはOK以外のステータスでは例外をthrowするため、失敗したフレームリクエストはすべて報告されます。
フォールバックがすでに送信された後に遅延フレームが失敗した場合、ボディのバイト列がすでに送出されているため、サーバーはページをエラーレスポンスに差し替えることができません。エラーは報告され、フォールバックは画面に残ります。サーバーログにはそのエラーが記録されますが、ユーザーが何を見ていたかはわかりません。そのギャップを埋めるのがセッションリプレイです。ユーザーがページの他の部分を使い続けながら、いつまでも解消されない「Loading…」メッセージを前に待たされている様子を確認できます。停止した領域が最後まで描画されなくても意味が通じるように、フォールバックを設計してください。
Remix 2からの移行
Remix 2のアプリをRemix 3に移行するのは、アップグレードではなく書き直しです。ルートは型付きのルートマップへ、データローディングはコントローラーのアクションへ、UIはremix/componentにあるRemix独自のコンポーネントランタイムへと移行します。ガイドに移行に関する章はありません。Remix 2のインプレースでのアップグレードパスはReact Router v7であり、Remix 3は別のコードベースです。Remix 1/2向けに書かれたこちらのNext.jsからRemixへの移行ガイドのような記事は、Remix 3のガイドとしてではなく、あくまで背景知識として参照してください。
まとめ
Remix 3のデータローディングは、GETルート、データをawaitしてResponseを返すコントローラーのアクション、そして独立して読み込みや失敗が発生する領域のためのフレーム、という要素に集約されます。まずは1ページ分のルートをapp/routes.tsにマッピングし、そのデータフェッチをコントローラーに移して、フォールバックが必要な領域を判断するところから始めてみてください。新しいフレームワークの活用についてさらに知りたい方は、Remix 3がAIコーディングエージェント向けにどのように設計されているかもご覧ください。
よくある質問
Remix 3はReactを使用していますか?
いいえ。Remix 3はJSX構文を維持していますが、ReactやReact Router上では動作しません。remix/componentからエクスポートされる独自のコンポーネントモデルを使用しています。コンポーネントは、ハンドルを受け取って一度だけ実行される外側のセットアップ関数と、更新のたびに実行される内側のレンダー関数で構成されます。useStateなどのReactフックはこのモデルに含まれないため、Reactのコンポーネントライブラリは変更を加えない限りRemix 3では動作しません。
Remix 3ではフォーム送信やミューテーションはどのように動作しますか?
app/routes.tsでform()ルートヘルパーを使ってフォームを宣言します。デフォルトでは、同じURLに2つのリーフが作成されます。フォームを表示するindexのGETルートと、送信を処理するactionのPOSTルートです。両方のリーフは同じコントローラーに属します。作成、更新、削除が成功した後は、ページを再読み込みしてもフォームが再送信されないよう、303リダイレクトを返すことがRemix 3のガイドで推奨されています。
Remix 3のコントローラーのアクションはHTMLではなくJSONを返せますか?
はい。コントローラーのアクションは、Response.json()、リダイレクト、ファイル、ストリームなど、あらゆるWeb Responseを返すことができます。Remix 3のForms and Mutationsガイドでは、オートコンプリート、ポーリング、自己完結型のウィジェット、API、サービス間の呼び出しといったケースでJSONを使うことを提案しています。一方で、HTMLレスポンスとリダイレクトですでに機能しているフォームに、別途JSONによるミューテーションの経路を追加することは推奨していません。
Remix 3はReact Router v7と同じものですか?
いいえ。Remix v2は、2024年11月にリリースされたReact Router v7にFramework Modeとして統合されました。これが、ローダーとReactを使い続けたいRemix 2アプリのためのアップグレードパスです。Remix 3は2025年5月に発表された別のプロジェクトで、独自のコンポーネントモデルの上に構築されています。Remix 3へ移行するには、ルート、データローディング、UIを書き直す必要があります。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k