mcpField
R
remix-v2-data-flow

remix-v2-data-flow

github.com/existential-birds開発8541 回インストール11 週間前に更新
リポジトリ

remix-v2-data-flow について

Remix v2 アプリを開発する人向けのデータフロー参照スキルです。データ取得、変更、再検証、ストリーミング、処理中状態を扱い、判断ガイドとコード例で API の選択や移行時の注意点を示します。

主な機能

  1. 01loader と action のパターン
  2. 02型付きデータとレスポンス処理
  3. 03defer によるストリーミングとエラー境界
  4. 04自動再検証の指針
  5. 05v1 から v2 への処理中状態の変更点

活用シーン

  1. 01loader とクライアント側エフェクトの選択
  2. 02ルートの変更処理とリダイレクトの実装
  3. 03ストリーミングデータのエラー処理
  4. 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.md
defer-await.md
loaders.md
revalidation.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> — ステータス/ヘッダーを指定できる、使いやすい JSON Response ラッパーです。
  • 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 の比較
  1. このルートの最初のレンダリングを正しく行うためにデータが必要ですか(SSR、プリフェッチ、アクション後の自動再検証など)?
    • Pass → loader + useLoaderData<typeof loader>()。終了。
    • Fail → ステップ 2。
  2. マウント後のユーザー操作、タイマー、またはサブスクリプションによってフェッチが開始されますか(ルートへの移動ではない)?
    • Pass → useEffect/イベントハンドラー。終了。
    • Fail → loader + 再検証を優先してください。エフェクト内でナビゲーションを再現しないでください。
json() と raw Response と defer() の比較
  1. ページをすばやくレンダリングする間に、返されるフィールドの一部をストリーミングする必要がありますか(遅いクエリ、コストの高い集計など)?
    • Pass → defer({ critical: await…, slow: promiseWithoutAwait }) + <Suspense><Await>…</Await></Suspense>。終了。
    • Fail → ステップ 2。
  2. カスタムのステータスコード、カスタムヘッダー、または明示的な TypedResponse<T> の型指定が必要ですか?
    • Pass → json(data, init)(3xx の場合は redirect(url, init))。終了。
    • Fail → ステップ 3。
  3. JSON 以外の本文(バイナリ、プレーンテキスト、ストリーミングファイル)が必要ですか?
    • Pass → raw new Response(body, init) を作成してください。終了。
    • Fail → json(data) をデフォルトとして使用してください。これはオブジェクトのペイロードに関する、文書化された v2 の契約です。
<Form>/ルートアクションと useFetcher の比較
  1. URL または履歴スタックを変更する必要がありますか(ブックマーク/共有/戻る操作で前の画面に戻る)?
    • Pass → ルートの action に送信する <Form method="post">。終了。
    • Fail → ステップ 2。
  2. ミューテーションを同じルート内で行いますか(インライン編集、リスト行の切り替え、ポップオーバー、楽観的 UI など)?
    • Pass → useFetcher()/fetcher.Form/fetcher.submit()。終了。

追加ドキュメント

  • ローダー: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 クイック差分

項目v1v2
ナビゲーションフックuseTransition()useNavigation()
送信データの形状transition.submission.formMethodフラットな nav.formMethod / nav.formData
formMethod の大文字小文字"post""POST"(大文字)
Fetcher の型フィールドfetcher.type === "actionSubmission"fetcher.state + fetcher.formData に基づく分岐
Loader 引数の型LoaderArgs / ActionArgsLoaderFunctionArgs / ActionFunctionArgs
データの返却json(data) が必須json(data) が引き続き文書化された契約

コメント