关于 remix-v2-data-flow-review
面向 TypeScript Remix v2 项目的代码审查技能,聚焦路由数据加载、提交、重验证与流式数据模式,并提供检查清单和误报豁免规则。
主要功能
- 01检查 loader 与 action 数据流
- 02审查表单验证和敏感字段泄露
- 03检查重验证与待处理状态
- 04审查 defer 和流式数据用法
- 05提供审查门槛与有效模式
适用场景
- 01审查 Remix v2 路由模块的数据处理
- 02检查 action 的验证与成功分支
- 03排查重验证或延迟数据模式问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill remix-v2-data-flow-review安装后在 Claude Code 中输入/remix-v2-data-flow-review确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/actions.mddefer-await.mdloaders.mdrevalidation.md详细介绍
Remix v2 数据流代码审查
针对导入 @remix-run/* 的 TypeScript 路由模块。请参阅 remix-v2-data-flow,了解规范模式。
范围
- 范围内:
app/routes/下导出loader、action、shouldRevalidate或headers的路由模块;使用useLoaderData、useActionData、useNavigation、useFetcher、useRevalidator、<Await>的组件。 - 范围外:表单易用性(
<Form>标记、无障碍性、useFetcherUI 模式)→ 由remix-v2-forms-review覆盖。路由模块约定、文件命名、嵌套路由、错误边界位置 → 由remix-v2-routing-review覆盖。 - 预期导入:服务器工具使用
@remix-run/node(或@remix-run/cloudflare/@remix-run/deno);钩子和组件使用@remix-run/react。
快速参考
| 问题类型 | 参考 |
|---|---|
| 在 loader 中执行变更、缺少校验、泄露服务器字段、抛出基本类型值、缺少参数检查 | references/loaders.md |
未校验的 FormData、成功时使用 json 而非 redirect、缺少错误分支、泄露 actionData | references/actions.md |
遗留的 useTransition v1 用法、缺少待处理状态、无差别地设置 shouldRevalidate: false、误用 useRevalidator | references/revalidation.md |
对已经快速的数据使用 defer、缺少 <Suspense>、<Await> 没有 errorElement、等待本应流式传输的数据 | references/defer-await.md |
审查清单
- 首次渲染所需的数据由
loader提供,而非通过useEffect获取 - Loader 只读取数据;写入操作放在
action中 - 使用前先校验
request.formData()的结果(zod/valibot/invariant) - Loader/action 的返回值是经过筛选的 DTO——不包含密码哈希、令牌或
internal_*字段 -
useLoaderData<typeof loader>()使用类型注解形式(而非as Foo) - 404 / 认证的短路处理使用
throw抛出Response(或json/redirect),绝不使用普通Error或字符串 - 成功的 action 返回
redirect(...)(PRG);校验失败时返回json({ errors }, { status: 400 }) - Action 处理成功和错误两种分支;不要静默地
return null - 使用前通过
invariant/ zod 检查params.foo - 待处理 UI 读取
useNavigation()/fetcher.state——不使用useTransition -
formMethod比较使用大写形式("POST",而非"post") -
shouldRevalidate默认返回defaultShouldRevalidate;选择不重新校验时,应限定范围并说明理由 - 仅当至少有一个 promise 会以流式方式传输时才使用
defer()(传入前不要await) - 每个
<Await>都由<Suspense>包裹,并且设有errorElement -
useRevalidator().revalidate()仅用于焦点、轮询或 SSE 场景——不要在<Form>提交或fetcher.submit后立即调用(Remix 已会重新校验)。
有效模式(不要标记)
以下是正确的 Remix v2 用法,不应报告为问题:
- 用于客户端专属数据的
useEffect— Loader 在服务端运行;localStorage、window尺寸、IntersectionObserver和仅限浏览器的 API 应放在useEffect中。 loader返回null— Loader 可以合理地返回null(例如,可选资源不存在);只有在应该throw一个 404 时才标记。- 将
useLoaderData<typeof loader>()用作类型注解 —<typeof loader>是传入SerializeFrom<T>的泛型参数,并非as风格的类型断言。不要将其标记为“不安全的类型转换”。 - 直接返回
new Response(body, init)— v2 路由可以返回任意Response;json()是一种便捷封装,并非必需。非 JSON 响应体(二进制、文本、流)正确地不使用json()。 - 在 action 中使用
return redirect(...)— 在 action 中,return redirect(...)和throw redirect(...)都合法;只有在非 action 辅助函数中需要退出调用函数时,才必须使用throw。 - 声明
loader时不带request参数 — Loader 可以只解构所需内容({ params }、{ context },或不带参数的());未使用的参数不是错误。 - 无关 action 之后重新验证父级
loader— 这是 Remix 的默认行为,并非代码异味。只有在存在shouldRevalidate且其行为错误时才标记。 - Action 成功时返回
json({ errors }, { status: 400 })— 这是规范的验证错误模式(会让表单路由保持渲染,并显示字段错误)。这与“成功时不重定向”的反模式不同。 - 将
useRevalidator用于焦点变化、轮询或跨标签页同步 — 这些都是文档中说明的用例;只有在<Form>提交或fetcher.submit后立即手动调用revalidate(),而 Remix 本身已会重新验证时,才标记。 SerializeFrom引起的类型变化 — 反序列化后,Date被标记为string、Map被标记为{},这是正确的线格式行为,并非类型错误。
视具体情境而定的规则
仅在符合特定情境时标记以下问题:
| 问题 | 仅当以下条件成立时才标记 |
|---|---|
缺少 loader(改用 useEffect) | 数据可在服务端获取,且不是读取仅限浏览器的 API |
loader 返回原始 ORM 对象 | 对象包含审查者不会贴到截图中的字段(密码、令牌、内部标记) |
Action 成功时返回 json | 该 action 通过 <Form> 调用并导致 URL 变化——而非通过 useFetcher 调用 |
| 缺少待处理状态 UI | 文件其他位置没有用于驱动同一界面的 nav.state / fetcher.state 引用 |
shouldRevalidate 返回 false | 函数体没有条件,或从未引用 formAction / currentParams / nextParams |
手动调用 useRevalidator().revalidate() | 该调用紧跟 Remix 管理的变更操作(<Form> 提交、fetcher.submit)——而非焦点变化、轮询或 websocket |
使用了 defer() | defer({...}) 载荷中的每个 Promise 在调用前都已使用 await |
硬性门槛(编写问题发现前)
按顺序执行这些检查。在本批次所有门槛均通过之前,请勿起草面向用户的问题发现。
-
位置证据 — 通过: 每个问题都列出路由模块的仓库路径,以及你读取的文件中的行范围或简短原文引用(不能凭记忆或仅根据差异猜测)。如果没有指向
export async function loader|action的路径,则不得报告 Loader/Action 问题。 -
豁免检查 — 通过: 对于每个问题,你都能用一行说明它为何不属于 有效模式(不要标记) 的范围。特别要确认:
useEffect并非用于加载仅限客户端的数据;裸Response返回值并非有意采用非 JSON 格式;返回null的loader并非合法的可选读取。 -
类型注解与类型断言检查 — 通过: 在标记 Loader/Action 数据消费中的“非安全类型转换”之前,确认代码使用的是
as(断言),而非useLoaderData<typeof loader>()(注解)或useActionData<typeof action>()(注解)。泛型形式是文档中规定的安全方式,不得标记。 -
v1 遗留项检查 — 通过: 在标记“缺少待处理状态”之前,先在文件中搜索
useTransition、transition.submission、fetcher.type、formMethod === "post"或formMethod==='post'(小写,空格或引号形式不限),以及LoaderArgs/ActionArgs。如果存在,这属于 v1 遗留迁移问题,而非缺少功能的问题——请据此标注。 -
协议 — 通过: 已针对本次审查完成 review-verification-protocol 中的审查前验证清单。
何时加载参考资料
- 审查
loader函数体、返回结构、参数、抛出错误或敏感字段泄漏 → references/loaders.md - 审查
action函数体、FormData 验证、成功/错误分支或 PRG 重定向 → references/actions.md - 审查
useNavigation/useTransition迁移、shouldRevalidate或useRevalidator用法 → references/revalidation.md - 审查
defer()、<Await>、<Suspense>或流式处理决策 → references/defer-await.md
审查问题
- 首次渲染所需的数据是否在
loader中获取,还是被放在会破坏 SSR 和重新验证的useEffect中? - 每个 loader 是否都返回经过筛选的 DTO,还是原始 ORM 记录(包含
password、token、internal_*字段)泄漏到了浏览器? - 每个 action 是否都在访问数据库前,使用 schema 验证
request.formData()? - 每个 action 的成功分支是否都使用
redirect(...),以确保刷新/返回行为正确(PRG)? - 消费端代码是否使用
useLoaderData<typeof loader>()(注解),而非useLoaderData() as Foo(断言)? - 是否仍有 v1 遗留项(
useTransition、transition.submission、fetcher.type、小写的formMethod、LoaderArgs/ActionArgs)? shouldRevalidate是否返回字面量false,或者使用defaultShouldRevalidate并仅针对特定情况选择退出?- 是否仅在传入至少一个尚未解析的 Promise 时才使用
defer(),并且每个<Await>是否都包裹在带有errorElement的<Suspense>中?
补充文档
- Remix v2 数据流的标准模式及 v1 → v2 差异 → remix-v2-data-flow
- 审查前验证清单 → review-verification-protocol
提交问题发现前
完成硬性门槛(尤其是第 5 条),然后仅报告仍通过 review-verification-protocol 审查前检查的问题。