关于 remix-v2-forms-review
用于审查 Remix v2 表单代码的技能,检查提交方式、待处理状态、文件上传、数据验证及多变更路由模式。它采用上下文相关规则和核验门槛,减少误报。
主要功能
- 01Form 与 fetcher 用法检查
- 02待处理状态审查
- 03上传与 FormData 验证检查
- 04多变更路由模式检查
- 05证据与例外核验门槛
适用场景
- 01审查 Remix 表单改动
- 02发现绕过路由 action 的变更
- 03检查逐行待处理指示器
- 04审计上传和输入验证
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill remix-v2-forms-review安装后在 Claude Code 中输入/remix-v2-forms-review确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/form-vs-fetcher.mdmulti-action-routes.mdpending-state.mduploads-validation.md详细介绍
Remix v2 表单代码审查
规范模式请参阅 remix-v2-forms。此技能用于标记违规;配套技能则讲解这些模式。
快速参考
| 问题类型 | 参考资料 |
|---|---|
手动使用 fetch()、原生 <form>、错误选择 <Form> 或 useFetcher | references/form-vs-fetcher.md |
使用 useState 设置加载标志、使用 useNavigation 管理每行状态、缺少待处理状态 | references/pending-state.md |
上传时内存使用无上限、缺少 encType、未验证的 FormData、镜像乐观状态 | references/uploads-validation.md |
| 未采用意图模式而导致路由泛滥、PUT/DELETE 没有 PE 回退方案 | references/multi-action-routes.md |
审查清单
- 应用内的变更使用
<Form>或<fetcher.Form>,绝不使用fetch()/axios - POST 变更时,从
@remix-run/react导入<Form>(而不是使用原生<form>) - URL 不应改变时(行切换、内联编辑)使用
useFetcher - URL 应改变时(创建、删除后返回列表)使用
<Form>+redirect(...) - 待处理状态从
useNavigation()或fetcher.state派生,绝不使用useState - 每行的待处理状态使用该行自己的
fetcher.state(而不是页面全局的useNavigation) - 调用
useNavigation()时检查navigation.formAction,以限定为预期路径 - 乐观 UI 直接读取
fetcher.formData/navigation.formData(不镜像状态) - 成功时,action 返回
redirect(...);仅在出错或停留在同一页面时返回json() - 每个文件上传表单都设置
<Form encType="multipart/form-data"> -
unstable_createMemoryUploadHandler始终设置maxPartSize;大文件使用磁盘/流处理器 - FormData 值在传入数据库前经过验证/类型转换(不要使用
form.get(x) as string) - 同一条路由上的多个变更使用意图模式,而不是拆分成多条路由
-
method="put|patch|delete"应注明仅支持 JS,或改写为 POST + 意图 - 将
nav.formMethod/fetcher.formMethod与大写字符串("POST"、"GET")比较;v2 的v2_normalizeFormMethod默认返回大写值——使用=== "post"会导致条件永远不匹配。
有效模式(不要标记)
以下是正确的 Remix v2 用法,不应报告:
- 不带
action属性的<Form>——按照约定,会向当前 URL 提交;action是可选的。 - GET
<Form>——适用于搜索/筛选界面;会将表单字段作为 URL 搜索参数传给 loader,且不会调用 action。大多数“规范性”规则(意图、redirect、encType)仅适用于 POST 表单。 - 在同一页面上使用多个
useFetcher()实例——每次调用都会返回独立的提交通道;这是针对不同行进行并行变更的有意设计。 - 在事件处理程序中使用
useSubmit()——适用于自动保存、键盘快捷键或onChange触发器的正确程序化提交方式。 - 在提交期间读取
fetcher.formData——这是预期用法,也是规范的乐观数据来源。 - 提交后
useActionData中的数据仍然存在——这是已知行为;它会返回上一次 action 的结果,直到下一次导航或 action。 - 在
<Form>上使用navigate={false}——会将其转换为 fetcher 表单;效果等同于不持有 fetcher 引用的<fetcher.Form>。 parseMultipartFormData/ 上传处理器上的unstable_前缀——在 v2 中是永久保留的;不要将其标记为“不稳定 API”。
上下文相关规则
仅当满足所列条件时才标记:
| 问题 | 仅当以下情况时标记 |
|---|---|
使用原生 <form> 而非 <Form> | 方法为 POST,且路由有 action — GET 表单和外部 URL 表单均可接受 |
| 缺少待处理状态 | 表单为 POST,且组件中任何位置都没有读取 useNavigation() / fetcher.state |
创建后 action 返回 json({ ok: true }) | 路由是“/new”或创建界面 — 同页编辑表单返回 JSON 是合理的 |
method="put" / "patch" / "delete" | 该界面需要渐进增强(公共应用)— 管理后台/仅限 JS 的工具可在文档中说明后选择不采用 |
unstable_createMemoryUploadHandler 没有大小限制 | 上传内容包含用户控制的文件(而非固定大小的内部工件) |
| 每种变更操作使用单独路由 | 这些变更操作针对相同资源,且身份验证规则兼容 — 具有不同规则的同级资源不在此列 |
使用 useNavigation() 时未设置 formAction 过滤条件 | 组件包含其他导航界面(侧边栏 <Link>、同级表单),这些界面会触发误报 |
将 fetcher.formData 镜像到状态中 | 被镜像的值用于驱动用户可见元素(按钮标签、计数、切换开关)— 本地的“is-editing”标记无关 |
硬性门槛(撰写问题前)
按顺序执行。在以下所有门槛都通过前,不要撰写面向用户的问题报告,且这些门槛必须适用于你即将报告的这一批问题。
-
位置证据 — 通过条件: 每个问题都列出仓库路径,以及你读取过的文件中的行范围或简短的逐字引文(不得凭记忆或仅根据 diff 猜测)。注明路由模块、组件,以及存在时的
action。 -
豁免检查 — 通过条件: 对于每个问题,你都能用一行说明它为何不属于 有效模式(不要标记) 中的豁免情况,也不符合 上下文相关规则 中的任何适用条目。
-
表单方法检查 — 通过条件: 在标记缺少 intent、缺少
encType、缺少redirect或缺少待处理状态之前,已确认表单的method="post"(或put|patch|delete)。GET 表单可用于搜索/筛选,并会触发 loader,而非 action — 将 POST 表单规则套用到这些表单上会造成误报。 -
协议 — 通过条件: 已针对本次审查完成 review-verification-protocol 中的报告前验证清单。
补充文档
- Form 与 fetcher 的误用 — 手动
fetch()、原生<form>、使用错误原语:references/form-vs-fetcher.md - 待处理状态反模式 —
useState标记、页面级与逐行状态、完全缺失:references/pending-state.md - 上传与 FormData 验证 — 没有大小限制的处理器、缺少
encType、未验证的键、镜像的乐观状态:references/uploads-validation.md - 多 action 路由 — intent 模式违规、没有 PE 回退的 PUT/DELETE、缺少提交按钮:references/multi-action-routes.md
- 规范模式 — 参见同级的 remix-v2-forms
何时加载参考文档
- 审查调用
fetch()/axios/ 原生<form>的表单,或在<Form>与useFetcher之间进行选择 → form-vs-fetcher.md - 审查加载标记、旋转图标、禁用按钮逻辑、逐行待处理状态 → pending-state.md
- 审查文件上传、
unstable_*处理器、FormData 解析、乐观 UI → uploads-validation.md - 审查包含多个变更操作、intent 字段、PUT/DELETE 方法的路由 → multi-action-routes.md
审查问题
- 应用内的每项变更是否都通过路由
action完成(没有手动调用fetch())? <Form>与useFetcher的选择是否取决于 URL 是否应该改变?- 待处理状态是否由
useNavigation()/fetcher.state派生(绝不使用useState)? - 每行的加载指示器是否绑定到该行自己的
fetcher.state(而非页面级的useNavigation)? - 文件上传表单是否设置了
encType="multipart/form-data"并使用了有界处理器? - FormData 的值在传入数据库前是否经过验证?
- 同一资源上的多个变更是否使用意图模式,而非拆分到不同路由?
- POST 表单是否至少有一个可用于渐进增强的实际提交按钮?
误报说明
- 在非路由组件中渲染的
<Form>仍然关联到最近路由的action——在标记“缺少 action”之前,请先查看路由文件。 - 成功提交后
useActionData()返回数据是预期行为;这些数据会持续存在,直到下次导航。只有在成功横幅无条件显示且没有关闭方式时,才应标记。 - 以别名导入
Form的代码(例如import { Form as RemixForm }) 仍然是 Remix 组件——应根据导入来源匹配,而不是本地名称。
提交问题前
完成 硬性检查(尤其是检查 4),然后只报告仍通过 review-verification-protocol 报告前检查的问题。