用 Claude Code 开发 Remix/React Router:loader 与 action 实战
用 Claude Code 构建 Remix 风格应用:loader、action、错误边界、SEO 与审查提示。
2026 年理解 Remix 的方式
现在谈 Remix 开发,不能只看旧的 Remix v2 示例。Remix 的很多框架能力已经进入 React Router v7,官方 Remix 文档也会把最新框架能力指向 React Router。因此,用 Claude Code 写 Remix 风格应用时,最好先说明项目是“现有 Remix v2 维护”,还是“React Router v7 Framework Mode 的新项目”。如果只说“帮我写 Remix”,Claude Code 很容易混用旧的@remix-run/*导入、新的react-router导入,以及普通前端fetch写法。
给初学者的理解方式是:Remix/React Router 的核心,是按路由放置loader和action,让服务器逻辑和 UI 靠得更近。loader负责页面显示前读取数据,action负责表单提交和数据变更,组件负责渲染,错误边界负责把失败限制在当前路由。这样做的好处是,代码生成后可以直接要求 Claude Code 审查“这个路由的读取、写入、错误和 SEO 是否完整”。
本文以 React Router v7 Framework Mode 为前提,构建一个可以复制的迷你应用:商品列表、商品详情和联系表单。示例覆盖三个常见场景:用loader读取数据、用action处理表单、用ErrorBoundary处理 404 和异常,并加入 SEO、审查提示、常见坑和商业转化入口。本文更新时参考了Remix Docs、React Router v7 发布说明、Route Module docs、Error Boundaries docs和Form API。
flowchart LR
A["URL / route"] --> B["loader 读取数据"]
B --> C["UI component 渲染"]
C --> D["Form 提交"]
D --> E["action 校验并写入"]
E --> B
B --> F["ErrorBoundary 处理读取失败"]
E --> F
先决定使用哪种模式
React Router v7 可以用 Declarative、Data、Framework 三种方式。接近 Remix 体验的是 Framework Mode:路由模块、服务端渲染、loader、action、类型生成和部署结构都在框架内组织。如果是新业务系统,这通常是最清晰的选择。
| 场景 | 建议 | 给 Claude Code 的说法 |
|---|---|---|
| 新业务应用 | React Router v7 Framework Mode | 使用create-react-router和 route module |
| 现有 Remix v2 | 保持现有导入方式 | 按当前@remix-run/*风格修改 |
| 现有 React SPA | 先局部引入 Data Mode | 先把一个页面迁到 loader/action |
| 带表单的营销页 | Framework 或 SPA Mode | 先确认是否需要 SSR 和服务端 action |
这个决定非常重要。Claude Code 写得很快,但如果边界不清,它会把不同年代的写法合在一起。本文示例全部按新 React Router v7 Framework Mode 写,同时用 Remix 风格解释。
创建可运行的小项目
先创建项目,再添加路由。商品列表、商品详情和联系表单足够验证核心模式。
npx create-react-router@latest rr-claude-shop
cd rr-claude-shop
npm install
npm run dev
// 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;
数据层先放在服务器专用文件中,之后可以替换成数据库或内部 API。
// 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适合读取页面初次渲染所需的数据。它比在组件里用useEffect再请求更容易管理初始状态、错误和 SEO。下面的列表页读取查询参数,在服务端过滤商品,并在同一路由中放置title和meta。
// 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>
);
}
场景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 };
}
真实项目还需要 CSRF、限流、垃圾信息过滤、邮件通知和 CRM 同步。提示词不要只写“做一个表单”,而要写“用 action 校验输入、显示字段错误、提交中禁用按钮、成功后显示结果”。
场景3:用错误边界控制失败范围
如果商品不存在,当前商品详情路由应该显示 404,而不是让整个页面崩掉。路由级错误边界正是 Remix 风格设计的优势。
// 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 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>;
}
审查时要确认两点:404、400 这种预期错误要给用户下一步;未知错误不能把error.stack、环境变量、数据库信息暴露到浏览器。
SEO 与内部链接
Remix/React Router 的 route module 很适合把 SEO 和页面逻辑放在一起。列表页写清楚“可以比较什么”,详情页把商品名放在标题前面,联系页说明可以咨询什么。React 19 之后的新项目可以优先使用组件中的<title>和<meta>,旧项目如果已使用meta()导出,也应保持一致。
内部链接也要在内容设计中考虑。本文可以连接到Claude Code React 开发、Claude Code API 开发和错误处理模式,让读者从一个路由示例自然进入更大的工程主题。
Claude Code 审查提示词
请审查这个 React Router v7 Framework Mode 的 route module。
重点检查:
1. loader 是否返回了秘密信息、内部字段或过大的数据
2. action 是否有服务端校验、字段错误、提交中状态和成功状态
3. ErrorBoundary 是否区分 404/400/未知错误,且没有泄露 stack
4. Form 是否在没有 JavaScript 时仍可基本提交
5. title/description 是否唯一且适合搜索
6. 是否混用了旧 Remix v2 import 与 React Router v7 import
请按严重程度列出问题,并给出最小修复差异。
常见失败
第一,混用旧导入和新导入。现有 Remix v2 项目应尊重原结构,新项目则应跟随 React Router v7 文档。第二,loader返回过多数据。凡是不需要显示给用户的成本、内部备注、管理员标记,都不该进入返回值。第三,只相信浏览器校验。type="email"不是安全边界,action必须重新校验。第四,把错误边界写成调试页面,把 stack 直接显示给用户。第五,把 SEO 当成自动发生的事情,导致重复标题和空 description。第六,不理解 action 后的 loader 重新验证,导致一个小表单触发太多重请求。
转化路径与实际结果
这个主题适合作为商业入口,因为它连接了表单、SEO、数据读取、错误处理和部署,读者通常已经接近真实项目。个人练习可以先看免费速查表,团队需要 React Router v7、Remix v2 维护、表单设计和 Claude Code 审查流程时,可以进入Claude Code 培训与导入咨询。需要可复用提示词和清单时,可以查看产品列表。
Masa 在本地小项目中测试了商品列表搜索、商品详情 404、联系表单校验和成功显示。模糊地说“用 Remix 做表单”时,Claude Code 容易混入旧导入和客户端fetch。明确说“Framework Mode、route module、loader/action、ErrorBoundary、title/meta”后,生成的差异更小,也更容易审查。结论是:先固定路由职责,再让 Claude Code 写代码,后续返工会少很多。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。