React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React Hook Form处理表单

React Hook Form 优雅处理表单使用指南

作者:oil欧哟

这篇文章主要为大家介绍了React Hook Form 优雅处理表单使用指南,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

受控组件与非受控组件

受控组件

先说说受控组件,以 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 方法。因此在维护复杂表单时,使用受控组件会有很大的额外代码量。

为了解决受控组件带来的问题,我们可以使用非受控组件来构建表单。受控组件主要有以下三个优点

React Hook Form 是什么?

React Hook Form 是一个基于 React 的 轻量级表单验证库。它使用了 React Hook API,让表单验证变得简单、易用、高效。React Hook Form 的主要目标是提供一个简单易用的表单验证解决方案,同时还能保持高性能和低开销。

React Hook Form 的特点都在官网首页 react-hook-form.com 中以可交互的形式展示,包括了以下几点:

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>
  );
}

咱们来分析一下这段代码:

这里我们不需要定义任何 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 函数是用来注册表单输入组件的,当组件注册之后,React Hook Form 会自动收集该组件的值,并根据验证规则进行验证。 register 函数会返回一个对象,其中包含了一些属性和方法,例如:

const { ref, onChange, onBlur, name } = register("firstName");

ref 属性是一个引用,指向该输入组件的 DOM 元素,onChangeonBlur 是回调函数,用于处理该组件的值变化和失去焦点事件,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 ,只需要将 useFormContextFormProvider 搭配使用,就可以将一个嵌套表单的逻辑分离成多个组件进行处理,且可以在父级组件提交时统一获取并处理数据。

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;

分析一下上边这段代码:

这段代码的核心就是 useFieldArray,它专门用于处理表单列表的场景,使用时我们将 useForm 返回的 control 传入 useFieldArray hook 中,并为这个列表定义一个名字,hook 会为我们返回一些操作列表的方法,在遍历渲染列表时,我们将每一个子项单独进行注册就可以实现表单列表的动态数据更改了。

需要注意的是,当使用 useFieldArray 处理表单中的数组字段时,每个字段都必须有一个 唯一的 key 值,这样才能正确地进行数组的添加、删除、更新等操作。如果数组中的字段没有 key 值,useFieldArray 会自动为每个字段生成一个随机的 key 值。

在内部实现上,useFieldArray 使用了 useFormContext 将 FormProvider 提供的 registerunregistersetValue 函数传递给了 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 对象中包含了一些属性,如 valueonChangeonBlur 等,这些属性传递给子组件,用于设置和更新表单控件的值,以及处理表单控件的事件,如用户输入、聚焦、失焦等。

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处理表单的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文