Claude CodeでRemix/React Router開発:loader・actionを安全に実装する実践ガイド
Remix/React RouterをClaude Codeで実装。loader、action、エラー境界、SEO、レビュー依頼まで初心者向けに解説。
Remix開発で今おさえるべき前提
2026年に「RemixをClaude Codeで書く」と言うときは、Remix v2だけを指すより、Remix由来の考え方を取り込んだReact Router v7のFramework Modeまで含めて考える方が実務に合います。Remix公式ドキュメントにも、最新のRemixはReact Router v7であり、新しいフレームワーク機能はReact Router docsから始めるように案内されています。既存のRemix v2アプリを保守する場合も、新規に近い構成を作る場合も、この流れを知らずに古い@remix-run/*の記事だけを見ると、依頼文もコードも少しずれます。
初心者向けに言うと、Remix/React Router系の強みは「ルート単位でloaderとactionを置き、サーバーとUIを近づける設計」です。loaderは画面を表示する前に必要なデータを読む場所、actionはフォーム送信などでデータを書き換える場所です。API層、状態管理、画面コンポーネントが散らばりすぎないため、Claude Codeに「このルートの読み込み、送信、エラー、SEOをまとめて見て」と頼みやすくなります。
この記事では、Claude CodeにRemix/React Router開発を頼むときの実務手順を、コピペで動くミニアプリの形で整理します。フォーム送信、loaderでのデータ取得、エラー境界、SEO/メタ情報、レビュー依頼プロンプト、失敗例までまとめます。公式情報としてRemix Docs、React Router v7の発表、Route Module docs、Error Boundaries docs、Form APIを確認しています。
flowchart LR
A["URL / route"] --> B["loader: read data"]
B --> C["UI component"]
C --> D["Form submission"]
D --> E["action: validate and write"]
E --> B
B --> F["ErrorBoundary when reading fails"]
E --> F
どのモードで作るかを先に決める
React Router v7には、大きくDeclarative、Data、Frameworkの3つの使い方があります。Remixの体験に近いのはFramework Modeです。Viteベースの開発環境、サーバーレンダリング、ルートモジュール、loader、action、型生成、デプロイ用の構成がまとまります。
| 状況 | 選び方 | Claude Codeへの伝え方 |
|---|---|---|
| 新規の業務アプリ | React Router v7 Framework Mode | create-react-router前提でroute moduleを作って |
| 既存のRemix v2保守 | 既存構成を維持しつつ差分実装 | @remix-run/*の現行コードに合わせて |
| 既存React SPAの段階移行 | Data Modeから導入 | まず特定画面だけloader/actionへ寄せて |
| 静的に近いLP | Framework ModeまたはSPA Mode | SSR要否とフォーム送信先を先に確認して |
Claude Codeに頼む前に、この表のどれかを明示します。「Remixで作って」だけだと、古いRemix v2のimport、React Router v7のimport、SPA寄りのfetch実装が混ざることがあります。今回は新規に近い構成として、React Router v7のFramework Modeを前提にします。
動くミニプロジェクトを作る
まず、商品一覧、商品詳細、問い合わせフォームだけの小さなアプリを作ります。ここではloader、action、エラー境界、SEOメタを一通り試せます。
npx create-react-router@latest rr-claude-shop
cd rr-claude-shop
npm install
npm run dev
ルートを明示するため、app/routes.tsを次のようにします。
// app/routes.ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("products", "routes/products.tsx"),
route("products/:productId", "routes/products.$productId.tsx"),
route("contact", "routes/contact.tsx"),
] satisfies RouteConfig;
データ取得は、最初はDBではなくサーバー専用モジュールに閉じます。*.server.tsに置くと、ブラウザ側へ混ざってはいけない処理だと分かりやすくなります。
// app/data/products.server.ts
export type Product = {
id: string;
name: string;
description: string;
price: number;
};
const products: Product[] = [
{
id: "starter",
name: "Claude Code Starter Kit",
description: "Small prompts and review checklists for the first team rollout.",
price: 9800,
},
{
id: "team",
name: "Team Workflow Pack",
description: "Route reviews, test prompts, and deployment checklists for teams.",
price: 29800,
},
];
const leads: Array<{ id: string; email: string; message: string }> = [];
export async function listProducts(query = "") {
const q = query.trim().toLowerCase();
if (!q) return products;
return products.filter((product) =>
`${product.name} ${product.description}`.toLowerCase().includes(q),
);
}
export async function getProduct(productId: string) {
return products.find((product) => product.id === productId) ?? null;
}
export async function saveLead(input: { email: string; message: string }) {
const lead = { id: crypto.randomUUID(), ...input };
leads.push(lead);
return lead;
}
ユースケース1:loaderで商品一覧を読む
loaderは、画面を描画する前に必要なデータを集める関数です。検索クエリ、認証済みユーザー、商品一覧、権限などをここで読みます。Reactコンポーネント内でuseEffectから取りに行くより、初期表示とエラー処理をルート単位で整理しやすくなります。
// app/routes/products.tsx
import { Form, Link, useLoaderData, useNavigation } from "react-router";
import { listProducts } from "~/data/products.server";
export async function loader({ request }: { request: Request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q") ?? "";
const products = await listProducts(q);
return { q, products };
}
export default function ProductsRoute() {
const { q, products } = useLoaderData<typeof loader>();
const navigation = useNavigation();
const searching = navigation.location?.pathname === "/products";
return (
<main>
<title>Products | Claude Code Shop</title>
<meta
name="description"
content="Browse Claude Code workflow products and team enablement kits."
/>
<h1>Products</h1>
<Form method="get" role="search">
<label>
Search
<input name="q" defaultValue={q} placeholder="workflow" />
</label>
<button type="submit">{searching ? "Searching..." : "Search"}</button>
</Form>
<ul>
{products.map((product) => (
<li key={product.id}>
<Link to={`/products/${product.id}`}>{product.name}</Link>
<p>{product.description}</p>
<strong>{product.price.toLocaleString()} JPY</strong>
</li>
))}
</ul>
</main>
);
}
ここでのポイントは、SEO/メタ情報も同じルートに近づけていることです。React RouterのRoute Module docsでは、React 19以降は組み込みの<title>と<meta>要素の利用が推奨されています。既存プロジェクトでmeta() exportを使っている場合はそれでも動きますが、新規コードでは公式ドキュメントに合わせて書く方が説明しやすいです。
ユースケース2:actionで問い合わせフォームを処理する
actionは、フォーム送信や削除ボタンのようなデータ変更を受ける場所です。<Form>はJavaScriptが読み込まれる前でもHTMLフォームとして動き、読み込み後はReact Routerが送信状態や再検証を扱います。Claude Codeにフォームを作らせるときは、バリデーション、エラーメッセージ、送信中表示、成功表示を必ず同時に依頼します。
// app/routes/contact.tsx
import { Form, useActionData, useNavigation } from "react-router";
import { saveLead } from "~/data/products.server";
type ActionData =
| { ok: true; leadId: string }
| { ok: false; errors: { email?: string; message?: string } };
export async function action({ request }: { request: Request }): Promise<ActionData> {
const formData = await request.formData();
const email = String(formData.get("email") ?? "").trim();
const message = String(formData.get("message") ?? "").trim();
const errors: { email?: string; message?: string } = {};
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = "Enter a valid email address.";
}
if (message.length < 20) {
errors.message = "Tell us at least 20 characters about your situation.";
}
if (Object.keys(errors).length > 0) {
return { ok: false, errors };
}
const lead = await saveLead({ email, message });
return { ok: true, leadId: lead.id };
}
export default function ContactRoute() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const submitting = navigation.state === "submitting";
return (
<main>
<title>Contact | Claude Code Shop</title>
<meta
name="description"
content="Ask about Claude Code workflow products and team training."
/>
<h1>Contact</h1>
<Form method="post">
<label>
Email
<input name="email" type="email" aria-describedby="email-error" />
</label>
{actionData && !actionData.ok && actionData.errors.email ? (
<p id="email-error">{actionData.errors.email}</p>
) : null}
<label>
Message
<textarea name="message" rows={6} aria-describedby="message-error" />
</label>
{actionData && !actionData.ok && actionData.errors.message ? (
<p id="message-error">{actionData.errors.message}</p>
) : null}
<button type="submit" disabled={submitting}>
{submitting ? "Sending..." : "Send"}
</button>
</Form>
{actionData?.ok ? <p>Thanks. Lead ID: {actionData.leadId}</p> : null}
</main>
);
}
実案件では、ここにCSRF対策、rate limit、スパム判定、メール通知、CRM連携を足します。Claude Codeには「フォームを作って」ではなく、「actionで入力検証し、不正値は400相当のUIに戻し、成功時はIDを返し、送信中はボタンを無効化して」と細かく頼む方が安定します。
ユースケース3:エラー境界で壊れる範囲を小さくする
Remix/React Router系の良さは、エラーをルート単位で受け止められることです。商品詳細のloaderで商品が見つからない場合、全画面を白くせず、そのルートのErrorBoundaryだけを表示できます。
// app/routes/products.$productId.tsx
import { data, isRouteErrorResponse, Link, useLoaderData } from "react-router";
import { getProduct } from "~/data/products.server";
export async function loader({ params }: { params: { productId?: string } }) {
const productId = params.productId;
if (!productId) {
throw data("Missing product id", { status: 400 });
}
const product = await getProduct(productId);
if (!product) {
throw data("Product not found", { status: 404 });
}
return { product };
}
export default function ProductDetailRoute() {
const { product } = useLoaderData<typeof loader>();
return (
<main>
<title>{product.name} | Claude Code Shop</title>
<meta name="description" content={product.description} />
<h1>{product.name}</h1>
<p>{product.description}</p>
<strong>{product.price.toLocaleString()} JPY</strong>
<p>
<Link to="/contact">Ask about this product</Link>
</p>
</main>
);
}
export function ErrorBoundary({ error }: { error: unknown }) {
if (isRouteErrorResponse(error)) {
return (
<main>
<h1>{error.status === 404 ? "Product not found" : "Could not load product"}</h1>
<p>{error.data}</p>
<Link to="/products">Back to products</Link>
</main>
);
}
return (
<main>
<h1>Unexpected error</h1>
<p>Please try again later.</p>
</main>
);
}
公式のエラー境界ドキュメントでは、Framework Modeではエラーがルート境界に渡されること、404のような想定内のケースではdata()をstatus付きでthrowできることが説明されています。注意点は、スタックトレースや環境変数をユーザー向けUIに出さないことです。Claude Codeが<pre>{error.stack}</pre>を本番UIに残したら、レビューで止めます。
ユースケース4:SEO/メタをルート設計に含める
Remix/React Router系では、SEOを後付けにせず、ルート実装の一部として扱えます。商品一覧なら検索意図、商品詳細なら商品名、問い合わせなら相談導線を<title>と<meta name="description">に入れます。
| ルート | titleの考え方 | descriptionの考え方 |
|---|---|---|
/products | 一覧であることを明示 | 何を比較できるかを書く |
/products/:productId | 商品名を先頭に置く | 誰向けの商品かを短く書く |
/contact | 相談・問い合わせを明示 | 何を相談できるかを書く |
Claude CodeにSEOまで依頼するときは、「titleは32文字前後、descriptionは100文字前後、検索意図は初心者向け、重複titleを避ける」のように制約を渡します。既存サイトの内部リンクも一緒に見せると、React開発ガイド、API開発ガイド、エラーハンドリングパターンへ自然につながる導線を作れます。
Claude Codeへのレビュー依頼プロンプト
以下をそのまま貼ると、Claude Codeがコード生成だけでなく、設計レビューまでしやすくなります。
React Router v7 Framework Modeのroute moduleをレビューしてください。
前提:
- Remix由来の設計として、routeごとにloader/action/UI/ErrorBoundaryを近づけたい
- loaderは表示前データ取得、actionはフォーム送信とデータ更新に限定したい
- React 19以降の新規コードなので、SEOは可能なら<title>/<meta>をroute component内で扱う
確認してほしいこと:
1. loaderで秘密情報や不要な巨大データを返していないか
2. actionで入力検証、失敗時UI、送信中状態、成功時表示がそろっているか
3. ErrorBoundaryが404/400/予期せぬ例外を分け、stackやsecretを出していないか
4. FormがJavaScriptなしでも最低限送信できるか
5. title/descriptionがページごとに重複していないか
6. 古いRemix v2のimportとReact Router v7のimportが混在していないか
出力:
- 問題点を重大度順に列挙
- 修正差分を最小で提案
- 最後にnpm run typecheck / npm run buildで確認すべき観点を書く
よくある失敗例
1つ目は、古い記事のimportをそのまま混ぜることです。既存Remix v2なら@remix-run/reactや@remix-run/nodeが出てきますが、React Router v7のFramework Modeではreact-routerと@react-router/dev/routesを使う例が中心です。移行中のプロジェクトでは既存方針を尊重し、新規なら公式の現行docsへ合わせます。
2つ目は、loaderで返しすぎることです。サーバー側で読めるからといって、ユーザーの内部メモ、原価、管理者用フラグまで返すと、ブラウザのネットワークタブから見えます。UIに必要な形へ絞って返してください。
3つ目は、actionに検証を置かないことです。HTMLのrequiredやtype="email"は便利ですが、ブラウザを通らないリクエストを止めるものではありません。サーバー側のactionで必ず検証します。
4つ目は、エラー境界を「全部catchして終わり」にすることです。404、入力不足、権限不足、予期せぬ例外を同じ文言にすると、ユーザーも運用者も原因を追えません。ただし、ユーザー向けには詳細すぎるstackを出さないようにします。
5つ目は、メタ情報が親子ルートで自動マージされると思い込むことです。React Routerのdocsでは、route moduleのmeta配列は最後にマッチしたルートが置き換える挙動に注意が必要です。組み込み<title>/<meta>を使う場合も、各ルートで重複と不足を確認します。
6つ目は、フォーム送信後の再検証を理解しないことです。Framework Modeではフォーム送信後にloader dataが再検証されます。便利ですが、重いloaderを複数ぶら下げていると、送信のたびに余計な通信が増えます。必要に応じてshouldRevalidateを検討します。
収益導線としてのRemix記事
Remix/React Routerは、単なる技術記事より「実装相談」につなげやすいテーマです。理由は、フォーム、SEO、サーバー処理、デプロイ、エラー対応が1つのルートに集まるからです。初心者は「Reactは分かるが、問い合わせフォームや認証やSEOをどうつなげるか」で止まりやすく、そこにClaude Codeの使い方を組み合わせると、研修やテンプレート商品の導線になります。
自分で試す場合は、まず無料チートシートでClaude Codeへの依頼文を増やしてください。チームでReact Router v7、Remix v2移行、フォーム設計、レビュー運用まで整えるならClaude Code研修・導入相談が近道です。実装テンプレートやレビュー観点を手元に置きたい場合は商品一覧も見てください。
この記事で紹介した内容を実際に試した結果
Masaの手元では、上のproductsとcontactのルートを小さなReact Router v7プロジェクトに入れ、検索、商品詳細の404、問い合わせフォームの入力不足、成功表示まで確認しました。Claude Codeに最初から「Remixでフォームを作って」と頼んだときは、古いimportとfetch手書きが混ざりやすかったです。一方で「Framework Mode、route module、loader/action、ErrorBoundary、title/metaを同じルートでレビューして」と指定すると、差分がかなり読みやすくなりました。Remix系の開発では、Claude Codeに実装を急がせるより、ルート単位の責務を先に固定する方が、後から直す量が少なくなります。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。