关于 remix-v2-forms
面向 Remix v2 开发者的表单与 mutation 技能,梳理 action、表单和状态 Hook 的协作方式,帮助保留渐进增强与数据重新验证。
主要功能
- 01区分导航表单与就地提交
- 02使用 action 处理表单数据
- 03读取错误与待处理状态
- 04支持乐观界面和文件上传
- 05按场景选择 Remix API
适用场景
- 01创建记录后跳转到详情页
- 02在列表行内修改或切换状态
- 03显示全局或局部待处理指示
- 04实现带验证的文件上传
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill remix-v2-forms安装后在 Claude Code 中输入/remix-v2-forms确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/fetcher.mdform.mdintent-actions.mdoptimistic-ui.mduploads.md详细介绍
Remix v2 表单与变更
适用于 @remix-run/react@^2 路由模块
框架的标准变更原语。正确的 Remix v2 变更包括:一个 <Form method="post">(或
<fetcher.Form>)、一个用于解析 request.formData() 并返回
redirect(...) 或 json(...) 的 action,以及使用 useActionData()
(或 fetcher.data)读取错误、使用 useNavigation()(或 fetcher.state)
读取待处理状态的 UI。任何绕过此循环的做法——fetch()、原生
<form>、e.preventDefault() + 客户端状态——都会悄然牺牲
重新验证、渐进增强和防竞态转换。
快速参考
<Form> + action:
import { json, redirect, type ActionFunctionArgs } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
export async function action({ request }: ActionFunctionArgs) {
const form = await request.formData();
const email = String(form.get("email") ?? "");
if (!email.includes("@")) return json({ errors: { email: "Invalid" } }, { status: 400 });
await createUser({ email });
return redirect("/dashboard");
}
export default function Signup() {
const actionData = useActionData<typeof action>();
const nav = useNavigation();
const busy = nav.state !== "idle" && nav.formAction === "/signup";
return (
<Form method="post" replace>
<input name="email" type="email" />
{actionData?.errors?.email ? <em>{actionData.errors.email}</em> : null}
<button disabled={busy}>{busy ? "Signing up..." : "Sign Up"}</button>
</Form>
);
}
原语
| 名称 | 用途 |
|---|---|
来自 @remix-run/react 的 <Form> | 可导航、渐进增强的表单,会向路由 action 提交并触发整页重新验证 |
<Form navigate={false}> | “通过 fetcher 提交;不导航”的简写。等同于 <fetcher.Form>,但无需持有 fetcher 引用——适用于只需要待处理状态、不需要编程式句柄的场景 |
useFetcher() | 用于行内变更、列表行、弹出层的非导航式提交通道——重新验证行为相同,但不会更改 URL |
useFetchers() | 应用中所有进行中的 fetcher 状态的只读数组。可用于全局待处理指示器(顶部加载器),无需逐层传递 props。返回项不包含 Form/submit/load 方法——只有 formData、state 等。 |
useNavigation() | 观察页面级导航;是 <Form> 待处理状态的事实来源 |
useSubmit() | 编程式提交(onChange 自动保存、键盘快捷键)。接受 HTMLFormElement、FormData、普通对象(以表单编码),或通过 { encType: "application/json" } 编码为 JSON 的普通对象 |
useActionData<typeof action>() | 读取当前路由最近一次 action 的结果 |
状态转换:
useNavigation().state:非 GET 表单提交时为idle → submitting → loading → idle;GET 导航时为idle → loading → idle。useFetcher().state:idle → submitting → loading → idle。
**不对称之处:**GET 导航时,useNavigation 会跳过 submitting;useFetcher 不会——只有 fetcher.load() 会跳过它。<fetcher.Form method='get'> 和 fetcher.submit(..., {method:'get'}) 都会经过 submitting 状态。
关键模式
用 <Form> 导航,用 useFetcher 原地提交
<Form> 会更改 URL、添加历史记录,并重新验证所有 loader。
useFetcher 会执行相同的重新验证,但停留在当前 URL。
每次调用 useFetcher() 都会返回独立的提交通道,因此
两行同时提交时不会共享待处理状态。
在一个路由上处理多个操作的意图模式
使用一个 action,根据 formData.get("intent") 进行分支;每种操作使用不同的
<button name="intent" value="...">。只有被点击的
提交按钮的 name=value 会进入请求体。参见
references/intent-actions.md。
根据 formData 实现乐观 UI
提交时会同步填充 fetcher.formData 和 navigation.formData,并在
状态变为 idle 时清除。每次渲染时直接读取;切勿将其同步到本地 React 状态。参见
references/optimistic-ui.md。
文件上传需要 encType="multipart/form-data"
若未设置此项,request.formData() 会剥离文件数据,你得到的将是
文件名字符串,而不是 File。使用
unstable_parseMultipartFormData 和有大小限制的上传处理器进行解析。
unstable_ 前缀在 v2 中将永久保留。参见
references/uploads.md。
门槛(决策顺序)
按顺序回答。通过表示条件为真;选择同一行上的 API 并停止。
<Form> 与 useFetcher 的区别
- 变更后是否需要更改 URL(创建记录并跳转到
/records/:id、删除后返回列表、多步骤流程)?- 是 →
<Form method="post">+ 在 action 中使用redirect(...)。结束。 - 否 → 第 2 步。
- 是 →
- 是否要修改某一行、单元格、开关或子区域,同时让用户留在同一页面(收藏、点赞、增加数量、内联编辑)?
- 是 → 使用
useFetcher()和<fetcher.Form>。结束。 - 否 → 第 3 步。
- 是 → 使用
- 是否要在常规导航之外加载数据(弹出框内容、组合框结果、预加载)?
- 是 → 使用
fetcher.load(href)。结束。 - 否 → 默认使用
<Form>。导航是更稳妥的选择——开箱即用地支持重新验证和历史记录。
- 是 → 使用
硬性规则:对于针对自有 Remix 路由的应用内变更,绝不要使用 fetch() 或 axios。这会绕过 action 生命周期,并跳过 loader 重新验证。
待处理状态:useNavigation 与 useFetcher.state
- 待处理指示器是否是全局的(根组件中的页面加载动画、顶部加载条)?
- 是 → 在
root.tsx中使用useNavigation()(navigation.state !== "idle")。结束。 - 否 → 第 2 步。
- 是 → 在
- 变更是否通过
useFetcher发起?- 是 → 使用该 fetcher 的
fetcher.state。useNavigation()不会反映 fetcher 的活动状态。结束。 - 否 → 第 3 步。
- 是 → 使用该 fetcher 的
- 指示器是否限定于列表中的某一行或按钮,且每一行都有自己的 fetcher?
- 是 → 使用每行的
fetcher.state(或通过useFetchers()按 key 查找),以免其他行闪烁。结束。 - 否 → 第 4 步。
- 是 → 使用每行的
- 指示器是否限定于刚刚通过
<Form>提交的表单?- 是 → 使用
useNavigation(),并检查navigation.formAction === "/expected-path",以免无关导航触发局部加载动画。结束。 - 否 → 第 5 步。
- 是 → 使用
- 是否需要渲染乐观值?
- 是 → 读取
navigation.formData?.get("field")(页面表单)或fetcher.formData?.get("field")(fetcher)——只要state !== "idle",两者都会有值。结束。
- 是 → 读取
补充文档
<Form>组件:参阅 references/form.md,了解<Form>、原生<form>与fetch()的区别、渐进增强、 成功后重定向,以及通过useActionData显示验证错误。useFetcher:参阅 references/fetcher.md,了解 内联变更、列表操作、弹出框、fetcher.state、fetcher.data、fetcher.Form、fetcher.submit、fetcher.load。- 乐观 UI:参阅
references/optimistic-ui.md,了解
fetcher.formData和useNavigation.formData、何时应用,以及 失败时如何还原。 - 文件上传:参阅 references/uploads.md,了解
unstable_parseMultipartFormData、unstable_createMemoryUploadHandler、unstable_createFileUploadHandler和有界处理程序。 - 基于意图的 action:参阅
references/intent-actions.md,了解如何通过 FormData 的
intent字段在同一路由上执行多个 action。
对比
| 关注点 | <Form> | useFetcher | 原生 <form> | fetch() |
|---|---|---|---|---|
| URL 变更/历史记录条目 | 是 | 否 | 是(完整导航) | 否 |
| 无 JS 时可用 | 是 | 是 | 是 | 否 |
| 重新验证 loaders | 是 | 是 | 是(完整重新加载) | 否 |
| 待处理状态钩子 | useNavigation() | fetcher.state | 无 | 手动处理 |
| 乐观输入来源 | navigation.formData | fetcher.formData | 无 | 手动处理 |
| 应用内变更用例 | 创建/删除/多步骤 | 内联/行/开关 | 仅外部目标 | 绝不用来操作自有路由 |