React Hook Form 优雅处理表单使用指南
作者:oil欧哟
受控组件与非受控组件
受控组件
先说说受控组件,以 input 为例:
const [value,setValue] = useState('') <input value={value} onChange={(e)=> setValue(e.target.value)} />
在上面的代码中,我们通过通过自己维护一个 state 来获取或更新 input 输入的值,以这种方式控制取值的 表单输入元素 就叫做 受控组件。
非受控组件
那么什么是非受控组件呢?在 React 中,非受控组件是指表单元素的值由 DOM 节点直接处理,而不是由 React 组件来管理。如下例:
import React, { useRef } from 'react'; function UncontrolledForm() { const nameInputRef = useRef(null); const emailInputRef = useRef(null); const passwordInputRef = useRef(null); function handleSubmit(event) { console.log('Name:', nameInputRef.current.value); console.log('Email:', emailInputRef.current.value); console.log('Password:', passwordInputRef.current.value); event.preventDefault(); } return ( <form onSubmit={handleSubmit}> <label> Name: <input type="text" ref={nameInputRef} /> </label> <label> Email: <input type="email" ref={emailInputRef} /> </label> <label> Password: <input type="password" ref={passwordInputRef} /> </label> <button type="submit">Submit</button> </form> ); }
在这个例子中,我们使用 useRef
Hook 创建了一个 ref 对象,并将其赋值给每个 input 元素的 ref
属性。在 handleSubmit
函数中,我们使用 ref.current.value
来获取每个 input 元素的值。这里的每个input 元素都是非受控组件,因为它们的值由 DOM 节点直接处理,而不是由 React 组件来管理。
当然,这意味着当用户输入数据时,React 无法追踪表单元素的值。因此,当您需要访问表单元素的值时,您需要使用DOM API来获取它们。
为什么需要非受控组件
在 React 中,通常使用受控组件来处理表单。受控组件表单元素的值由 React 组件来管理,当表单数据发生变化时,React 会自动更新组件状态,并重新渲染组件。这种方式可以使得表单处理更加可靠和方便,也可以使得表单数据和应用状态之间保持一致。
但在实际的开发中,表单往往是最复杂的场景,有的表单有数十个字段,如果使用受控组件去构建表单,那么我们就需要维护大量 state,且 React 又不像 Vue 可以通过双向绑定直接修改 state 的值,每一个表单字段还需要定义一下 onChange
方法。因此在维护复杂表单时,使用受控组件会有很大的额外代码量。
为了解决受控组件带来的问题,我们可以使用非受控组件来构建表单。受控组件主要有以下三个优点
- 可以减少组件的 代码量和复杂度,因为非受控组件不需要在组件状态中保存表单数据。
- 可以更好地 处理大量表单数据,因为非受控组件可以让您直接操作DOM元素,而不需要将所有表单数据存储在组件状态中。
- 可以更容易地与第三方 JavaScript 库和表单处理代码集成,因为非受控组件使您能够使用 DOM API 或 ref 直接访问表单元素,而不是在 React 中重新实现所有的表单处理逻辑。
React Hook Form 是什么?
React Hook Form 是一个基于 React 的 轻量级表单验证库。它使用了 React Hook API,让表单验证变得简单、易用、高效。React Hook Form 的主要目标是提供一个简单易用的表单验证解决方案,同时还能保持高性能和低开销。
React Hook Form 的特点都在官网首页 react-hook-form.com 中以可交互的形式展示,包括了以下几点:
- 通过 React Hook 的方式减少使用时的代码量,简单易上手,并且移除了不必要的重复渲染:
- 隔离重复渲染,自组件重新渲染时不会触发父组件或兄弟组件的重新渲染:
- 订阅机制,与上一点相似,能够订阅单个输入和表单状态更新,而无需重新呈现整个表单:
- 组件渲染速度足够快,而且代码库非常小,压缩后只有几 KB 大小,不会对页面性能造成任何影响:
React Hook Form 的使用姿势
数据收集
先看看最基础的表单实现:
import React from "react"; import { useForm } from "react-hook-form"; function MyForm() { const { register, handleSubmit } = useForm(); const onSubmit = (data) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register("firstName")} /> <input {...register("lastName")} /> <button type="submit">Submit</button> </form> ); }
咱们来分析一下这段代码:
- 使用
useForm
函数创建一个表单对象,该函数返回一个包含register
和handleSubmit
等方法的对象。 - 在表单中定义两个输入框,使用
register
函数注册表单输入组件,并指定组件的名称为firstName
和lastName
。 - 使用 React Hook Form 提供的
handleSubmit
函数来管理表单的提交和数据验证。
这里我们不需要定义任何 state 即可在 submit 时获取到表单中的数据,接下来我们补充一下基本的表单验证和错误提示:
import React from "react"; import { useForm } from "react-hook-form"; function MyForm() { const onSubmit = (data) => { console.log(data); }; const { register, handleSubmit, formState: { errors } } = useForm(); return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register("firstName", { required: true })} /> {errors.firstName && <p>First name is required.</p>} <input {...register("lastName", { required: true })} /> {errors.lastName && <p>Last name is required.</p>} <button type="submit">Submit</button> </form> ); }
咱们再分析一下这段代码:
register
函数中可以指定表单输入组件的 验证规则 ,例如使用required
规则来验证输入框的必填项。- 在表单提交处理函数中,可以调用 React Hook Form 提供的
handleSubmit
函数来自动执行表单验证,并返回验证结果。只有表单验证通过才会执行onSubmit
方法。 - 如果表单验证失败,可以使用
errors
对象来获取每个表单输入组件的验证错误信息,并在 UI 上显示错误提示。
register
函数是用来注册表单输入组件的,当组件注册之后,React Hook Form 会自动收集该组件的值,并根据验证规则进行验证。 register
函数会返回一个对象,其中包含了一些属性和方法,例如:
const { ref, onChange, onBlur, name } = register("firstName");
ref
属性是一个引用,指向该输入组件的 DOM 元素,onChange
和 onBlur
是回调函数,用于处理该组件的值变化和失去焦点事件,name
是该组件的名称。
register
函数内部会创建一个管理表单输入组件的对象,包含了该组件的名称、引用、验证规则等信息。同时,还会将该对象保存在 React Hook Form 内部的一个数据结构中。
在表单提交时,React Hook Form 会遍历管理的所有表单输入组件,并收集它们的值,并根据其注册时定义的验证规则进行验证。
到这里,一个最基本的表单验证及提交就已经实现了。当然,在实际开发中,表单之所以复杂是由于各种条件渲染及表单嵌套引起的,那么我们接下来再看看使用 React Hook Form 如何处理这些场景。
表单嵌套
还是一样,先看看示例:
父级表单:
// ParentForm.jsx import React from "react"; import { useForm, FormProvider } from "react-hook-form"; import ChildForm from "./ChildForm"; function ParentForm() { const methods = useForm(); const { register, handleSubmit, formState: { errors } } = methods; const onSubmit = (data) => { console.log(data); }; return ( <FormProvider {...methods}> <form onSubmit={handleSubmit(onSubmit)}> <h2>Parent Form</h2> <label htmlFor="firstName">First Name</label> <input {...register("firstName", { required: true })} /> {errors.firstName && <p>First name is required.</p>} <label htmlFor="lastName">Last Name</label> <input {...register("lastName", { required: true })} /> {errors.lastName && <p>Last name is required.</p>} <ChildForm/> <button type="submit">Submit</button> </form> </FormProvider> ); } export default ParentForm;
子级表单:
import React from "react"; import { useFormContext } from "react-hook-form"; function ChildForm() { const { register, errors } = useFormContext(); return ( <div> <h2>Child Form</h2> <label htmlFor="childFirstName">Child First Name</label> <input {...register("child.firstName", { required: true })} /> {errors.child?.firstName && <p>Child first name is required.</p>} <label htmlFor="childLastName">Child Last Name</label> <input {...register("child.lastName", { required: true })} /> {errors.child?.lastName && <p>Child last name is required.</p>} </div> ); } export default ChildForm;
分析一下这两个组件的代码:
在 ParentForm
组件中,我们使用 useForm
hook 来获取表单的注册函数、表单状态等信息,并使用 FormProvider
组件将其传递给所有的子组件。
在 ChildForm
组件中,我们使用了 useFormContext
hook 来获取父表单的注册函数和表单状态。
这里的两个表单组件间并不需要咱们去单独定义 props ,只需要将 useFormContext
与 FormProvider
搭配使用,就可以将一个嵌套表单的逻辑分离成多个组件进行处理,且可以在父级组件提交时统一获取并处理数据。
FormProvider
是 React Hook Form 提供的一个组件,用于在 React 组件树中向下传递 useForm
hook 的实例。它创建了一个 React Context,并将 useForm
hook 的实例作为 Context 的值,然后通过 Context.Provider
组件将这个值传递给所有子组件.
而 useFormContext
则可以在子组件中获取到 FormProvider
提供的 useForm
hook 的返回值。在使用 useFormContext
时,不需要手动使用 Context.Provider
将值传递给子组件,而是可以直接从 useFormContext
中获取,简化嵌套表单的代码逻辑。
条件判断
import React from "react"; import { useForm } from "react-hook-form"; function ExampleForm() { const { register, handleSubmit, watch } = useForm(); const onSubmit = (data) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> <label htmlFor="hasAge">Do you have an age?</label> <select {...register("hasAge")}> <option value="yes">Yes</option> <option value="no">No</option> </select> {watch("hasAge") === "yes" && ( <> <label htmlFor="age">Age</label> <input {...register("age", { required: true, min: 18 })} /> {watch("age") && <p>You must be at least 18 years old.</p>} </> )} <button type="submit">Submit</button> </form> ); } export default ExampleForm;
我们在 hasAge
输入框上使用了一个简单的条件渲染:只有当用户选择了 "Yes" 时,才会渲染 age
输入框。然后使用 watch
函数来监听输入框的值,并在输入的值小于 18 时显示相应的错误信息。
watch
函数用来监听指定的输入并返回它们的值。在渲染输入值和进行条件渲染时经常用到。
表单列表
import React from "react"; import { useForm, useFieldArray } from "react-hook-form"; function ListForm() { const { register, control, handleSubmit } = useForm({ defaultValues: { list: [{ name: "" }, { name: "" }, { name: "" }] } }); const { fields, append, remove } = useFieldArray({ control, name: "list" }); const onSubmit = (data) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> {fields.map((field, index) => ( <div key={field.id}> <input {...register(`list.${index}.name`, { required: "This field is required" })} defaultValue={field.name} /> <button type="button" onClick={() => remove(index)}> Remove </button> </div> ))} <button type="button" onClick={() => append({ name: "" })}> Add Item </button> <button type="submit">Submit</button> </form> ); } export default ListForm;
分析一下上边这段代码:
- 在这个示例中,我们使用了
useForm
和useFieldArray
hook 来处理一个表单列表。其中list
属性是一个包含 3 个空对象的数组。 - 使用
fields.map
方法遍历fields
数组,渲染出每一个列表项。 - 使用
remove
方法为每个列表项添加了一个 "Remove" 按钮,使得用户可以删除不需要的列表项。我们还使用append
方法添加了一个 "Add Item" 按钮,可以添加新的列表项。
这段代码的核心就是 useFieldArray
,它专门用于处理表单列表的场景,使用时我们将 useForm 返回的 control 传入 useFieldArray hook 中,并为这个列表定义一个名字,hook 会为我们返回一些操作列表的方法,在遍历渲染列表时,我们将每一个子项单独进行注册就可以实现表单列表的动态数据更改了。
需要注意的是,当使用 useFieldArray 处理表单中的数组字段时,每个字段都必须有一个 唯一的 key 值,这样才能正确地进行数组的添加、删除、更新等操作。如果数组中的字段没有 key 值,useFieldArray
会自动为每个字段生成一个随机的 key 值。
在内部实现上,useFieldArray
使用了 useFormContext 将 FormProvider 提供的 register
、unregister
和 setValue
函数传递给了 useFieldArray
,然后在 useFieldArray 内部维护一个数组 state,保存当前的数组值和对数组的操作。
第三方组件
当需要与第三方UI组件(如<DatePicker />
、<Select />
、<Slider />
等)集成时,如果使用register
注册这些第三方UI组件,可能会遇到如无法正确更新表单数据、错误处理、性能差等问题。
因此,使用Controller
是一种更好的解决方案,可以将表单数据与 React Hook Form
状态管理集成在一起,并使用render
函数来直接渲染第三方UI组件。下面放个例子:
import React from "react"; import { useForm, Controller } from "react-hook-form"; import { TextField, Button } from "@material-ui/core"; function ControllerForm() { const { control, handleSubmit } = useForm(); const onSubmit = (data) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> <Controller name="firstName" control={control} defaultValue="" rules={{ required: true }} render={({ field }) => ( <TextField label="First Name" {...field} /> )} /> <Controller name="lastName" control={control} defaultValue="" rules={{ required: true }} render={({ field }) => ( <TextField label="Last Name" {...field} /> )} /> <Button type="submit" variant="contained" color="primary"> Submit </Button> </form> ); } export default ControllerForm;
control
是一个对象,它提供了一些方法和属性,通过使用 control
,我们可以将 React Hook Form 中的数据与实际渲染的表单组件进行绑定,从而让 React Hook Form 管理表单中所有的输入和校验逻辑。
field
是 <Controller>
组件通过 render
回调函数传递给其子组件的一个对象,field
对象中包含了一些属性,如 value
、onChange
、onBlur
等,这些属性传递给子组件,用于设置和更新表单控件的值,以及处理表单控件的事件,如用户输入、聚焦、失焦等。
Controller
的好处是可以将表单数据和表单状态统一管理,同时避免了对表单数据的手动处理。此外,它还可以优化表单的渲染性能,并提供更好的错误处理机制,因为它可以自动处理错误消息和验证规则。
Typescript 支持
React Hook Form 提供了完整的 TypeScript 支持:
import React from "react"; import { useForm, SubmitHandler } from "react-hook-form"; type FormValues = { firstName: string; lastName: string; age: number; }; function MyForm() { const { register, handleSubmit, formState: { errors }, } = useForm<FormValues>(); const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register("firstName", { required: true })} /> {errors.firstName && <span>This field is required</span>} <input {...register("lastName", { required: true })} /> {errors.lastName && <span>This field is required</span>} <input {...register("age", { required: true, min: 18 })} /> {errors.age && ( <span> {errors.age.type === "required" ? "This field is required" : "You must be at least 18 years old"} </span> )} <button type="submit">Submit</button> </form> ); }
我们使用 FormValues
类型定义表单数据类型,并在 useForm
钩子中使用 FormValues
泛型接口。这使得我们可以在注册表单控件时提供正确的类型定义,并在 handleSubmit
函数中提供正确的表单数据类型。还可以使用泛型来定义错误消息的类型,可以用于准确地描述表单控件的错误状态,并提供适当的错误消息。提高代码的可读性和可维护性。
总结
除了上述的一些表单使用姿势,在官方的 react-hook-form.com/advanced-us… 页面还可以看到一些高级特性的示例,例如 Schema 解析为表单,实现表单组件测试,表单与 API 间的数据转换 等等。
总的来说,React Hook Form 提供了一种简单、灵活的方式来管理表单状态,支持非受控组件方式以及各种复杂的表单场景,同时也提供了 Typescript 支持,是一个值得尝试的表单管理库。
以上就是React Hook Form 优雅处理表单使用指南的详细内容,更多关于React Hook Form处理表单的资料请关注脚本之家其它相关文章!