当你学习如何在 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 验证器,类似于 Yup 和 Joi 这样的验证器。但它与前两者不同,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 合并,它以拥抱 FormData
、Request
和 Response
等网络标准而闻名。当表单提交时,它的数据可以通过标准的 Request
实例在服务器上使用。根据 MDN,你可以从 request
中获取 FormData
。
这种 "Remix" 处理表单数据的方式与 JavaScript 标准一致:
// 在 Remix 中,action "捕获" POST/PUT/DELETE 请求
export async function action(request: Request) {
const formData = await request.formData()
// ...
}