React 和 FormData

原文信息: 查看原文查看原文

React and FormData

- Brad Westfall

当你学习如何在 React 中访问表单数据时,历史上你可能已经了解了受控和非受控字段。后来你可能开始使用像 Formik 或 React Hook Form 这样的第三方抽象,它们在底层使用受控或非受控技术。无论哪种方式,最终目标都是收集你的表单数据。使用受控字段时,你的数据就是你的 state。使用非受控字段时,你需要自己收集表单值,通常开发者选择 refs 来实现这一点:

function onSubmit(event: React.FormEvent) {
  event.preventDefault()

  // 使用 refs 收集非受控表单字段。这些 refs
  // 让我们可以直接访问 DOM 中的输入字段
  const formValues = {
    name: nameRef.current.value,
    email: emailRef.current.value
  }
}

在 React 中,所有表单字段必须是受控的或非受控的,因为你要么添加了 value prop,要么没有。FormData,自2010年以来一直是JavaScript 标准,是一种无论表单是受控的还是非受控的,都可以访问你的表单数据的方法,但大多数人更喜欢非受控的。

尽管从一开始就可以在 React 中使用 FormData,但我们看到在过去几年里它的受欢迎程度有所上升。稍后,我们将展示它如何被现代 React 19 特性采用和推动。

FormData

使用 FormData,你不需要 refs 来获取非受控表单的值。相反,你可以直接从 event.target 中读取表单值:

function onSubmit(event: React.FormEvent) {
  event.preventDefault()
  const formData = new FormData(event.target)

  const formValues = {
    name: formData.get('name'),
    email: formData.get('email')
  }
}

由于某些原因,TypeScript 如果你使用 event.target 会报错,并希望你使用 event.currentTarget。只是让你知道,这两个经常指向同一件事,大多数情况下使用哪个并不重要。但现在我们将使用 event.currentTarget,因为许多 React 开发人员正在使用 TypeScript。

添加 Names

确保为输入字段添加名称,以便 FormData 可以工作:

// ✅ 有效,因为输入有匹配的名称
const email = formData.get('email')
<input type="text" name="email" />

不使用 getters 访问数据

我们能不能就这样提取所有的表单数据,而不使用 getters?

// ❌ 不起作用
const formValues = { ...formData }

对象实例 formData 更不透明,不是我们可以与对象字面量混合的那种对象。如果我们 console.log 它,我们也不会看到值:

console.log(formData) // 输出:`FormData {} [[Prototype]]: FormData`

Object.fromEntries()

你可以避免使用 getters,并将值解包成一个更普通的对象,如下所示:

const formValues = Object.fromEntries(formData)
console.log(formValues) // 输出:{ name: 'my name', email: 'name@someemail.com' }

然而,使用 TypeScript 这样做并不能给你想要类型。

FormData 和 TypeScript 的问题

尽管输入字段的值将是一个字符串,如果用户没有输入任何值,它将是一个空字符串,TypeScript 却说 getter 返回的类型是 FormDataEntryValue | null

const quantity = formData.get('quantity')
typeof quantity // FormDataEntryValue | null

这可能导致很多繁琐的工作,只是为了简单地将用户输入转换为数字:

// 断言字符串或 null
const quantity = formData.get('quantity') as string | null

// 然后提供默认值以防返回了 falsy 的 null 值
const quantity = (formData.get('quantity') as string | null) || 0

// 现在我们可以传递给 parseInt 来获取用户输入的整数版本
const quantity = parseInt((formData.get('quantity') as string | null) || 0)

使用 Object.fromEntries 情况并不会更好。它们只知道它是一个具有未知数量的字符串键和 FormDataEntryValue 值的对象:

const formValues = Object.fromEntries(formData)
typeof formValues // { [k: string]: FormDataEntryValue }

Zod 解决的问题

Zod 是一个基于模式的 JavaScript 验证器,类似于 YupJoi 这样的验证器。但它与前两者不同,Zod 非常明确地编写为与 TypeScript 良好协作。由于这不是一个 Zod 教程,我们将尽可能简洁地传达 Zod 的强大之处。

这里使用 Zod 的想法是,你很可能无论如何都需要验证,为什么不在不使用断言的情况下获得更好的类型呢?

当你将这个不透明的 formValues 对象传递给模式验证器时,Zod 会根据你编写的模式(这里未显示)来验证它,但然后也会以符合你的模式规则的方式返回你的数据,但以类型安全的方式:

const formValues = Object.fromEntries(formData) // ❌ 类型:{ [k: string]: FormDataEntryValue }

const results = myFormSchema.safeParse(formValues)
if (results.success) {
  results.data // ✅ 类型:{ email: string, quantity: number }
  console.log(results.data.email) // name@someemail.com
  console.log(results.data.quantity) // 5
} else {
  // 处理 results.errors
}

FormData 和 React 19

现代 React API 正在鼓励你使用和学习 FormData。在 React 19 中,你可以省略 onSubmit,转而使用 action

function MyForm() {
  function formAction(formData: FormData) {
    // 我们得到的是 formData 实例,而不是 event
  }

  return <form action={formAction}>...</form>
}

当 React 调用你的 formAction 函数时,他们会向你传递一个 FormData 实例。我们在 React 19 的钩子中看到了类似 FormData 的使用,比如 useActionState

在框架中

Remix,很快将与 React Router 7 合并,它以拥抱 FormDataRequestResponse 等网络标准而闻名。当表单提交时,它的数据可以通过标准的 Request 实例在服务器上使用。根据 MDN,你可以从 request 中获取 FormData

这种 "Remix" 处理表单数据的方式与 JavaScript 标准一致:

// 在 Remix 中,action "捕获" POST/PUT/DELETE 请求
export async function action(request: Request) {
  const formData = await request.formData()
  // ...
}
分享于 2024-09-18

访问量 90

预览图片