通用表单方案 React Hook Form 的入门教程

1,423 阅读7分钟

背景

React Hook Form 是目前 React 社区非常出名的一个表单解决方案。使用它可以轻松与现有项目的表单单元整合,实现更少的代码、更多的功能。

如果你目前的项目中没有使用类似 Ant Design 这样内置表单解决方案的 UI 库,同时又需要提供表单功能支持,那么 React Hook Form 会是你的不二之选。

首先,我们先创建好一个 React Hook Form 初始项目,很快的。

安装 React Hook Form

使用 Vite 创建项目,安装 React Hook Form 依赖。

创建项目:

npm create vite react-hook-form-demo -- --template react
cd react-hook-form-demo

安装依赖:


# 安装 antd 包
npm install react-hook-form
npm install

使用 VS Code 打开:

code .

删除 src/index.css 中的内容,修改 src/App.jsx 文件内容如下:

function App() {

  const onSubmit = (event) => {
    event.preventDefault()
    console.log('Submitted', event)
  }

  return (
    <form onSubmit={onSubmit}>
      <div>
        <label htmlFor="first-name">
          First Name: <input type="text" id="first-name" name="firstName" />
        </label>
      </div>
      <div>
        <label htmlFor="last-name">
          Last Name: <input type="text" id="last-name" name="lastName" />
        </label>
      </div>
      <div>
        <button type="submit">Submit</button>
      </div>
    </form>
  )
}

export default App

启动项目,浏览器访问。

$ npm run dev

> react-hook-form-demo@0.0.0 dev
> vite


  VITE v5.3.1  ready in 466 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

我们至此便完成了 React Hook Form 项目的搭建,不过目前只是写了一个纯粹的 Form 表单,输入数据,提交一下,查看效果。

当然,你可以将 Form 表单数据通过 FormData 提取出来。修改 onSubmit 方法:

const onSubmit = (event) => {
  event.preventDefault()
  const formData = new FormData(event.target)
  console.log(Object.fromEntries(formData.entries()))
}

输入数据,提交查看效果。

不过,目前表单功能还比较简单,如果在此基础之上再增加默认值支持、校验判断、变动监听,写出来的样板代码就会非常多,很不方便。

接下来,我们就来着手改造,接入 React Hook Form,看它是帮助我们提升开发效率的。

改造普通的 Form 表单

实现,从 react-hook-form 中引入 useForm() Hook,这是 React Hook Form 核心功能 API。

import { useForm } from 'react-hook-form'

接着,再 App.jsx 中从 useForm() 中解析出 register 和 handleSubmit。

  • register 是应用在像 <input> 这样表单元素上的
  • handleSubmit 顾名思义,是用来处理提交的,它接收处理函数作为参数
const { register, handleSubmit } = useForm()

接下来如下所示,改造 <form> 表单。

+ const onSubmit = (formData) => {
+  console.log(formData)
+ }

- <form onSubmit={onSubmit}>
+ <form onSubmit={handleSubmit(onSubmit)}>
  <div>
    <label htmlFor="first-name">
-      First Name: <input type="text" id="first-name" name="firstName" />
+      First Name: <input type="text" id="first-name" {...register('firstName')} />
    </label>
  </div>
  <div>
    <label htmlFor="last-name">
-      Last Name: <input type="text" id="last-name" name="lastName" />
+      Last Name: <input type="text" id="last-name"  {...register('lastName')} />
    </label>
  </div>
  <div>
    <button type="submit">Submit</button>
  </div>
</form>

有 2 处变动。

  1. <input> 表单的 name 被移除,改用 register('firstName')/register('lastName')
    • register() 的返回值是一个对象,除了包含被移除的 name 属性之外,还包括 onBlur、onChange 还有 ref 引用,这样 <input> 表单就可以交由 React Hook Form 托管了

  1. 另外,原先的 onSubmit 函数被替换成 handleSubmit(),再被调用。这样 onSubmit 中接收的就是处理之后的表单数据了,无需手动通过 FormDate 获取

再次输入数据,点击提交。

当然,其他表单元素同样适应,比如 <select>

<form onSubmit={handleSubmit(onSubmit)}>
  <div>
    <label htmlFor="name">
      Name: <input type="text" id="name" {...register('name')} />
    </label>
  </div>
  <div>
    <label htmlFor="gender">
      <select id="gender" {...register("gender")}>
        <option value="female">female</option>
        <option value="male">male</option>
        <option value="other">other</option>
      </select>
    </label>
  </div>
  <div>
    <button type="submit">Submit</button>
  </div>
</form>

效果:

如此一来,我们就将一个普通表单改造成经由 React Hook Form 托管的表单了。

表单校验

目前,我们表单中的字段没有强制规则,提交时空值也会提交。

而使用 React Hook Form 的为这些元素提供校验也比较简单,就在调用 register() 时通过第 2 个参数指定校验规则。

<form onSubmit={handleSubmit(onSubmit)}>
  <label style={{ display: 'flex' }}>
    First Name: <input {...register("firstName", { required: true, maxLength: 20 })} />
  </label>
  <label style={{ display: 'flex' }}>
    Last Name: <input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
  </label>
  <label style={{ display: 'flex' }}>
    Age: <input type="number" {...register("age", { min: 18, max: 99 })} />
  </label>
  <input type="submit" />
</form>

以上的 required、maxLength、pattern 和 min、max 都是与现行 HTML 标准中规定的校验规则一一对应的。

以上,我们分别对各个字段做了如下的规则限制:

  • First Name:必填且最大长度 20
  • Last Name:必须由 26 英文字母构成
  • age:必须是介于 18~99 之间的数值
  1. 当我们没有按照规则填写数据时,React Hook Form 会自上而下 Focus 在第一个不符合规则的表单中,同时也不会触发 onSubmit

  1. 只有当所有表单都符合规则时,点击提交才会触发 onSubmit

  1. 对于校验不通过时,我们可以从 useForm 返回的 formState.errors 中获取到
function App() {
-  const { register, handleSubmit } = useForm()
+  const { register, handleSubmit, formState: { errors } } = useForm()

+ console.log(errors)

errors 中包含未通过规则校验的字段及对应规则名称。

  1. 当然对于想要更加细致报错的信息的,可以在每个表单之下做判断。比如下面这样:
<form onSubmit={handleSubmit(onSubmit)}>
  <input
    {...register("firstName", { required: true })}
    aria-invalid={errors.firstName ? "true" : "false"}
  />
  {errors.firstName?.type === "required" && (
    <p role="alert">First name is required</p>
  )}

  <input
    {...register("mail", { required: "Email Address is required" })}
    aria-invalid={errors.mail ? "true" : "false"}
  />
  {errors.mail && <p role="alert">{errors.mail.message}</p>}

  <input type="submit" />
</form>

提交查看效果:

无效

有效

  1. 当然,你看可以使用 watch 实时监听表数据
const { register, handleSubmit, watch, formState: { errors } } = useForm()

const watchAllFields = watch() // 监听所有字段
const watchSomeFields = watch(["firstName", "email"])
const watchOneField = watch("firstName")

console.log({ watchOneField, watchSomeFields, watchAllFields })

效果:

与现有封装的表单元素整合

如果你的项目中存在基于原始表单元素的基本封装组件,那么你同样可以与 React Hook Form 进行整合。

这类组件的整合分 2 种:一种是直接传入 register 在内部原始表单上进行绑定;另外一种就是在兼容 register 返回结果的结果对象。

先说第一种:

// The following component is an example of your existing Input Component
const Input = ({ label, name, register, required }) => (
  <>
    <label>{label}</label>
    <input {...register(name, { required })} />
  </>
)

如上所示,<Input> 组件从外部接收 register,配合其他传入的 props,组合最终的效果。使用起来是这样:

import { useForm } from 'react-hook-form'

const App = () => {
  const { register, handleSubmit } = useForm()

  const onSubmit = (data) => {
    console.log(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Input label="First Name" name="firstName" register={register} required />
      <input type="submit" />
    </form>
  )
}

不过,这种方式将 <Input> 的逻辑基于 React Hook Form 就绑定了,不是特别好。如果能支持这种就很好了:

<Input label="First Name" {...register('firstName', { required: true })} />

这种,其实就与原生 <input> 元素的使用一样了。

其实也不是很复杂,前面已经了解,register() 的返回值,共包含 4 个属性:name、onBlur、onChange 以及 ref。

这就表示,我们的 <Input> 只要实现以上 4 个 prop 即可。

// you can use React.forwardRef to pass the ref too
const Input = React.forwardRef(({ onChange, onBlur, name, label }, ref) => (
  <>
    <label>{label}</label>
    <input name={name} ref={ref} onChange={onChange} onBlur={onBlur} />
  </>
))

值得注意的是,传给 register 的 { required: true } 这些校验选型,最终是通过 ref 起作用的。现在再次输入、提交,发现依旧能正常工作,不过现在的 <Input> 的使用就不与 React Hook Form 强绑定了。

与现有 UI 库组件整合

以上,我们将 React Hook Form 与自己项目中抽象的组件进行了整合。显然,我们可以很根据 React Hook Form register() 方法的实际需要,调整我们的 props 涉及,来整合它。

不过对于像 Ant DesignMUI 这类本身就就有提供表单解决方案的 UI 库组件就没那么简单了——你不可能要求 Ant Design、MUI 的表单封装组件的 prop 去适配 React Hook Form 的 API。

React Hook Form 团队的同学也想到了,于是便提供了一个包装组件 Controller 来提供这类棘手的组件整合。

接下来,我们以 Ant Design 的表单组件整合为例。

首先,安装 Ant Design 依赖:

npm install antd

其次,我们从 react-hook-form 中引入 <Controller> 组件,从 antd 中引入 <Input>,并且从 useForm() 中引入 control(注意,此处就不是 register 了)。

import { Input } from 'antd'
import { useForm, Controller } from "react-hook-form"

const { control, handleSubmit } = useForm()

接着,拼合在一起。

<form onSubmit={handleSubmit(onSubmit)}>
  <Controller
    name='firstName'
    control={control}
    render={({ field }) => (
      <Input {...field} placeholder='First Name' />
    )}
  />
  <input type="submit" />
</form>

可以看见,Ant Design 的 <Input> 被作为 <Controller> 的内容进行渲染,同时,注入 <Controller> 提供的 filed 对象。你一定好奇 filed 里有啥。

<Controller
  name='firstName'
  control={control}
  render={({ field }) => {
    console.log('>>> field', field)
    return <Input {...field} placeholder='First Name' />
  }}
/>

没关系,我们打印一下:

发现跟之前返回 register() 是一样的,恰巧 Ant Design 的 Input 也与原生的 <input> 一样,我们直接将 field 传入即可。

此时,但我们在 <Input> 输入内容时,可以实时在控制台看到最新输出。

与此同时,<Controller> 还提供了 rules prop,用于指定校验规则,接收的参数同 register。

const { control, handleSubmit, formState: { errors } } = useForm()
  
<Controller
  name='firstName'
  control={control}
  rules={{ required: true }}
  render={({ field }) => {
    return <>
      <Input {...field} placeholder='First Name' />
      {errors.firstName?.type === "required" && (
        <p role="alert">First name is required</p>
      )}
    </>
  }}
/>

效果与与之前一样。

从上面的使用上来看,你会发现 React Hook Form 的 <Controller> 组件扮演的就是 Ant Design 中 <Form.Item> 的角色

总结

本文我们介绍了 React 社区中通用表单解决方案——React Hook Form。介绍它如何与原生元素、项目内封装元素、甚至与已有的 UI 库表单组件进行整合。

可以说,React Hook Form 的应用让我们节省了不少样板代码的书写,同时还能保证性能

好了,关于 React Hook Form 就介绍到这里了,希望对你目前的工作或学习有所帮助。再见。