mcpField
R
remix-v2-forms

remix-v2-forms

github.com/existential-birds开发8541 次安装11 周前更新
代码仓库

关于 remix-v2-forms

面向 Remix v2 开发者的表单与 mutation 技能,梳理 action、表单和状态 Hook 的协作方式,帮助保留渐进增强与数据重新验证。

主要功能

  1. 01区分导航表单与就地提交
  2. 02使用 action 处理表单数据
  3. 03读取错误与待处理状态
  4. 04支持乐观界面和文件上传
  5. 05按场景选择 Remix API

适用场景

  1. 01创建记录后跳转到详情页
  2. 02在列表行内修改或切换状态
  3. 03显示全局或局部待处理指示
  4. 04实现带验证的文件上传

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/existential-birds/beagle --skill remix-v2-forms

安装后在 Claude Code 中输入/remix-v2-forms确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
fetcher.md
form.md
intent-actions.md
optimistic-ui.md
uploads.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 的区别
  1. 变更后是否需要更改 URL(创建记录并跳转到 /records/:id、删除后返回列表、多步骤流程)?
    • 是 → <Form method="post"> + 在 action 中使用 redirect(...)。结束。
    • 否 → 第 2 步。
  2. 是否要修改某一行、单元格、开关或子区域,同时让用户留在同一页面(收藏、点赞、增加数量、内联编辑)?
    • 是 → 使用 useFetcher() 和 <fetcher.Form>。结束。
    • 否 → 第 3 步。
  3. 是否要在常规导航之外加载数据(弹出框内容、组合框结果、预加载)?
    • 是 → 使用 fetcher.load(href)。结束。
    • 否 → 默认使用 <Form>。导航是更稳妥的选择——开箱即用地支持重新验证和历史记录。

硬性规则:对于针对自有 Remix 路由的应用内变更,绝不要使用 fetch() 或 axios。这会绕过 action 生命周期,并跳过 loader 重新验证。

待处理状态:useNavigation 与 useFetcher.state
  1. 待处理指示器是否是全局的(根组件中的页面加载动画、顶部加载条)?
    • 是 → 在 root.tsx 中使用 useNavigation() (navigation.state !== "idle")。结束。
    • 否 → 第 2 步。
  2. 变更是否通过 useFetcher 发起?
    • 是 → 使用该 fetcher 的 fetcher.state。useNavigation() 不会反映 fetcher 的活动状态。结束。
    • 否 → 第 3 步。
  3. 指示器是否限定于列表中的某一行或按钮,且每一行都有自己的 fetcher?
    • 是 → 使用每行的 fetcher.state(或通过 useFetchers() 按 key 查找),以免其他行闪烁。结束。
    • 否 → 第 4 步。
  4. 指示器是否限定于刚刚通过 <Form> 提交的表单?
    • 是 → 使用 useNavigation(),并检查 navigation.formAction === "/expected-path",以免无关导航触发局部加载动画。结束。
    • 否 → 第 5 步。
  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.formDatafetcher.formData无手动处理
应用内变更用例创建/删除/多步骤内联/行/开关仅外部目标绝不用来操作自有路由

评论

支持 Markdown