remix-v2-data-flow について
Remix v2 アプリを開発する人向けのデータフロー参照スキルです。データ取得、変更、再検証、ストリーミング、処理中状態を扱い、判断ガイドとコード例で API の選択や移行時の注意点を示します。
主な機能
- 01loader と action のパターン
- 02型付きデータとレスポンス処理
- 03defer によるストリーミングとエラー境界
- 04自動再検証の指針
- 05v1 から v2 への処理中状態の変更点
活用シーン
- 01loader とクライアント側エフェクトの選択
- 02ルートの変更処理とリダイレクトの実装
- 03ストリーミングデータのエラー処理
- 04GET と POST フォームの処理中 UI の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill remix-v2-data-flowインストール後、Claude Code で/remix-v2-data-flowを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/actions.mddefer-await.mdloaders.mdrevalidation.md詳細
Remix v2 のデータフロー
クイックリファレンス
Loader + 型付き読み取り:
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ request }: LoaderFunctionArgs) {
const invoices = await db.invoice.findMany();
return json({ invoices });
}
export default function Invoices() {
// typeof loader is a type ANNOTATION (not assertion) — drives SerializeFrom<T>.
const { invoices } = useLoaderData<typeof loader>();
return <InvoiceList invoices={invoices} />;
}
Action + 成功後のリダイレクト(PRG):
import { json, redirect, type ActionFunctionArgs } from "@remix-run/node";
import { useActionData, Form } from "@remix-run/react";
export async function action({ request }: ActionFunctionArgs) {
const form = await request.formData();
const parsed = NewProject.safeParse(Object.fromEntries(form));
if (!parsed.success) return json({ errors: parsed.error.flatten().fieldErrors }, { status: 400 });
const project = await db.project.create({ data: parsed.data });
return redirect(`/projects/${project.id}`);
}
標準 API
ルートモジュールでは loader / action をエクスポートします。コンポーネントでは useLoaderData<typeof loader>() と useActionData<typeof action>() を使って結果を読み取ります。各 action の後、Remix はページ上の一致するすべてのルートの loader を自動的に再検証するため、手動でキャッシュを無効化しなくても UI はサーバーと一貫した状態に保たれます。
シグネチャ:
loader: ({ request, params, context }: LoaderFunctionArgs) => Response | Promise<Response>— サーバー専用の読み取り処理です。SSR 時と、fetch によるクライアント遷移時に実行されます。action: ({ request, params, context }: ActionFunctionArgs) => Response | Promise<Response>— GET 以外のリクエスト(POST/PUT/PATCH/DELETE)を処理するサーバー専用ハンドラーです。json(data, init?: number | ResponseInit): TypedResponse<typeof data>— ステータス/ヘッダーを指定できる、使いやすい JSONResponseラッパーです。redirect(url, init?: number | ResponseInit): TypedResponse<never>— 30x レスポンスです。デフォルトは 302 です。
インポート:Node ではサーバーユーティリティ(json、redirect、defer、型引数)に @remix-run/node を使用します。対象が異なる場合は、@remix-run/cloudflare または @remix-run/deno に置き換えます。フックとコンポーネントは @remix-run/react からインポートします。
型アサーションではなく型注釈
useLoaderData<typeof loader>() は型注釈であり、as 形式の型アサーションではありません。このジェネリックは SerializeFrom<typeof loader> に渡され、ワイヤー形式への変換を表します。Date は string になり、Map/Set は簡略化され、undefined のフィールドは取り除かれ、クラスメソッドは失われます。Date 型のフィールドに対して data.createdAt.getFullYear() を呼び出すと、実行時エラーになります。型はすでに string を示しています。
v2 で json() が省略可能な場合
v2 で基本的な契約が変更されたわけではありません。loader と action は Response を返す必要があります。json() は application/json を設定し、ステータス/ヘッダーを指定できる便利なラッパーです。v2 ではオブジェクトをそのまま返すこともできます(Remix が自動的に json() でラップします)が、ステータスやヘッダーを明示する場合や、明確な TypedResponse<T> 型付けを行う場合は json() が推奨されます。次の場合は json() を使用してください。
- 200 以外のステータスコードが必要な場合(例:バリデーションエラーに
{ status: 400 }を指定する)。 - カスタムヘッダーが必要な場合(キャッシュ、
Set-Cookie)。 - 明確な
useLoaderData<typeof loader>()の型推論に向けて、明示的なTypedResponse<T>が必要な場合。
早期終了のための throw
loader または action から Response を throw すると、データ関数は直ちに終了します。認証ガード(throw redirect("/login"))や 404(throw new Response("Not Found", { status: 404 }) または throw json({ message }, { status: 404 }))に使用します。通常の Error を throw しても、useRouteError()/isRouteErrorResponse() ではルートのレスポンスとして分類されません。
ストリーミング中の拒否:<Await errorElement> + useAsyncError()
defer() を通して渡された Promise が拒否された場合、<Await errorElement={...}> 境界がその場でエラーを捕捉します。これがないと、拒否はルートの ErrorBoundary に伝播し、ページ全体が破棄されるため、ストリーミングのメリットが失われます。errorElement 内では useAsyncError()(@remix-run/react から)を呼び出して拒否された値を読み取ります。これはストリーミングにおける useRouteError() に相当します。
function ReviewsError() {
const error = useAsyncError(); // typed as `unknown`
return <p>Failed to load reviews: {String(error)}</p>;
}
<Suspense fallback={<ReviewsSkeleton />}>
<Await resolve={reviews} errorElement={<ReviewsError />}>
{(r) => <ReviewList reviews={r} />}
</Await>
</Suspense>
機密データ
loader から返されるものはすべて JSON としてブラウザーに送信されます。返す前に安全な DTO({ id, email, name })に変換してください。Prisma の User 全体、パスワードハッシュ、API キー、内部フラグは絶対に返さないでください。loader はサーバー専用で実行されますが、戻り値は丸ごとクライアントに送信されます。
ミューテーションは Action に記述
loader は GET による遷移のたびに実行され、プリフェッチによって事前に呼び出される場合があります。また、自動再検証中にも再実行されます。永続状態を変更する処理はすべて action に記述し、<Form method="post"> または useFetcher 経由で実行してください。コンポーネントから直接 fetch() を呼び出して Remix ルートにアクセスすると、再検証、処理中の状態、プログレッシブエンハンスメントが迂回されます。代わりに useFetcher().submit()/useFetcher().load() を使用してください。
ルーティングで覚えておくべき注意点
- 一致するアクションのうち、最も深い階層のものだけが実行されます。 レイアウト内にネストされたインデックスルートは競合するため、
<Form action="/things?index" method="post" />のように指定してください。 useActionDataのスコープは現在のルートです。 親ルートや子ルートのアクション結果にはアクセスできません。共有するには、アクションを上位に移すか、keyを指定したuseFetcherを使用してください。- リーフローダーのヘッダーが優先されます。 Remix では、一致する
headersエクスポートのうち最も深いものだけが使用されます。parentHeadersを明示的にマージしない限り、親のキャッシュポリシーは無視されます。 - デフォルトの再検証では、アクション後にページ上のすべてのルートが再検証されます — パラメーターが変わっていないルートも対象です。コストの高い親ローダーを除外するには、
shouldRevalidateを使用してください。
保留中の状態(v1 → v2)
v2 では useTransition が削除されているため、useNavigation を使用してください。v2 では submission オブジェクトが navigation に直接フラット化されます(fetcher も同様で、nav.formData/nav.formMethod と fetcher.formData/fetcher.formMethod はどちらもフラットです)。v2 では formMethod が 大文字 になっています("post" ではなく "POST")。nav.formMethod === "post" のような比較は、一致しないまま気付かれないことがあります。また、fetcher.type もなくなっているため、fetcher.state と fetcher.formData の有無に基づいて分岐してください。
GET の送信は idle → loading → idle と遷移します。POST のフローは idle → submitting → loading → idle と遷移します。"submitting" の場合のみスピナーを表示すると、GET フォームを見逃します。GET の送信中も loading フェーズでは nav.formData が設定され、nav.formMethod === "GET" になります。そのため、フィルターフォームの保留中 UI は state === 'submitting' ではなく、formData の有無に基づいて分岐してください。useFetcher では、submitting は GET(<fetcher.Form method='get'> および fetcher.submit(..., {method:'get'}))と GET 以外の両方に適用されます。submitting をスキップするのは fetcher.load() のみです。これは、GET で submitting をスキップする useNavigation とは逆です。
判断フロー(選択の順序)
順番に回答してください。Pass は条件が真であることを意味します。同じ行にある API を選択し、そこで終了してください。
loader と useEffect の比較
- このルートの最初のレンダリングを正しく行うためにデータが必要ですか(SSR、プリフェッチ、アクション後の自動再検証など)?
- Pass →
loader+useLoaderData<typeof loader>()。終了。 - Fail → ステップ 2。
- Pass →
- マウント後のユーザー操作、タイマー、またはサブスクリプションによってフェッチが開始されますか(ルートへの移動ではない)?
- Pass →
useEffect/イベントハンドラー。終了。 - Fail →
loader+ 再検証を優先してください。エフェクト内でナビゲーションを再現しないでください。
- Pass →
json() と raw Response と defer() の比較
- ページをすばやくレンダリングする間に、返されるフィールドの一部をストリーミングする必要がありますか(遅いクエリ、コストの高い集計など)?
- Pass →
defer({ critical: await…, slow: promiseWithoutAwait })+<Suspense><Await>…</Await></Suspense>。終了。 - Fail → ステップ 2。
- Pass →
- カスタムのステータスコード、カスタムヘッダー、または明示的な
TypedResponse<T>の型指定が必要ですか?- Pass →
json(data, init)(3xx の場合はredirect(url, init))。終了。 - Fail → ステップ 3。
- Pass →
- JSON 以外の本文(バイナリ、プレーンテキスト、ストリーミングファイル)が必要ですか?
- Pass → raw
new Response(body, init)を作成してください。終了。 - Fail →
json(data)をデフォルトとして使用してください。これはオブジェクトのペイロードに関する、文書化された v2 の契約です。
- Pass → raw
<Form>/ルートアクションと useFetcher の比較
- URL または履歴スタックを変更する必要がありますか(ブックマーク/共有/戻る操作で前の画面に戻る)?
- Pass → ルートの
actionに送信する<Form method="post">。終了。 - Fail → ステップ 2。
- Pass → ルートの
- ミューテーションを同じルート内で行いますか(インライン編集、リスト行の切り替え、ポップオーバー、楽観的 UI など)?
- Pass →
useFetcher()/fetcher.Form/fetcher.submit()。終了。
- Pass →
追加ドキュメント
- ローダー:
loaderのシグネチャ、型付きのuseLoaderData、json()と生のResponseの違い、redirect()、例外のスロー、機密データのフィルタリング、パラメーターの処理については、references/loaders.md を参照してください。 - アクション:
actionのシグネチャ、FormData の解析、useActionData<typeof action>()、zod/valibot による検証、成功後のリダイレクトについては、references/actions.md を参照してください。 - Defer と Await:
defer()+<Await>+<Suspense>、ストリーミングが TTFB の改善に役立つ場合、エラー処理については、references/defer-await.md を参照してください。 - 再検証と保留状態:自動再検証、
shouldRevalidate、useRevalidator、useNavigation(および v1 でのuseTransitionからの名称変更)については、references/revalidation.md を参照してください。
v1 → v2 クイック差分
| 項目 | v1 | v2 |
|---|---|---|
| ナビゲーションフック | useTransition() | useNavigation() |
| 送信データの形状 | transition.submission.formMethod | フラットな nav.formMethod / nav.formData |
formMethod の大文字小文字 | "post" | "POST"(大文字) |
| Fetcher の型フィールド | fetcher.type === "actionSubmission" | fetcher.state + fetcher.formData に基づく分岐 |
| Loader 引数の型 | LoaderArgs / ActionArgs | LoaderFunctionArgs / ActionFunctionArgs |
| データの返却 | json(data) が必須 | json(data) が引き続き文書化された契約 |