2

表单是用户与网站和Web应用程序交互的重要组成部分。验证用户通过表单提交的数据是开发者的一项关键职责。

React Hook Form是一个帮助在React中验证表单的库。它是一个没有其他依赖项的精简库,性能优越,使用简单,开发者可以比使用其他表单库写更少的代码。

在本指南中,您将学习如何使用React Hook Form库在React中构建表单,而无需使用复杂的渲染属性(render props)或高阶组件(higher-order components)。

什么是React Hook Form?

React Hook Form与React生态系统中的其他表单库略有不同,它使用非受控输入(uncontrolled inputs)并通过ref进行管理,而不是依赖状态来控制输入。这种方法使表单的性能更高,并减少了重新渲染的次数。这也意味着React Hook Form与UI库可以无缝集成,因为大多数库都支持ref属性。

React Hook Form的体积非常小(压缩后仅为8.6 kB),并且没有任何依赖项。它的API非常直观,为开发者提供了无缝的体验。该库遵循HTML标准,通过基于约束的验证API进行表单验证。

安装React Hook Form,请运行以下命令:

npm install react-hook-form

如何在表单中使用React Hooks

在本节中,您将通过创建一个非常基本的注册表单来了解useForm Hook的基础知识。

首先,从react-hook-form包中导入 useForm Hook:

import { useForm } from "react-hook-form";

然后,在您的组件中如下使用该Hook:

const { register, handleSubmit } = useForm();

useForm Hook返回一个包含几个属性的对象。现在,我们只需要registerhandleSubmit

register方法帮助您将输入字段注册到React Hook Form中,以便它可以进行验证,并跟踪其值的变化。

要注册输入字段,我们将register方法传递给输入字段,如下所示:

<input type="text" name="firstName" {...register('firstName')} />

这种展开运算符语法是该库的新实现,它在使用TypeScript的表单中启用严格的类型检查。您可以在此了解有关React Hook Form中严格类型检查的更多信息。

React Hook Form 7.x版本之前,register方法是附加到ref属性上的,如下所示:

<input type="text" name="firstName" ref={register} />

注意,输入组件必须有一个name属性,并且其值应该是唯一的。handleSubmit方法顾名思义,负责处理表单提交。它需要作为form组件的onSubmit属性的值传递。

handleSubmit方法可以处理两个函数作为参数。当表单验证成功时,第一个传递的函数将与注册的字段值一起被调用。当验证失败时,第二个函数会被错误信息调用:

const onFormSubmit  = data => console.log(data);
const onErrors = errors => console.error(errors);

<form onSubmit={handleSubmit(onFormSubmit, onErrors)}>
 {/* ... */}
</form>

现在,您已经对useForm Hook的基本用法有了一个大致的了解,接下来让我们看看一个更实际的示例:

import React from "react";
import { useForm } from "react-hook-form";

const RegisterForm = () => {
  const { register, handleSubmit } = useForm();
  const handleRegistration = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(handleRegistration)}>
      <div>
        <label>Name</label>
        <input name="name" {...register('name')} />
      </div>
      <div>
        <label>Email</label>
        <input type="email" name="email" {...register('email')} />
      </div>
      <div>
        <label>Password</label>
        <input type="password" name="password" {...register('password')} />
      </div>
      <button>Submit</button>
    </form>
  );
};
export default RegisterForm;

如您所见,没有导入其他组件来跟踪输入值。useForm Hook使组件代码更简洁,更易于维护,而且由于表单是非受控的,您不必为每个输入传递onChangevalue等属性。

您可以使用任何其他UI库来创建表单。但首先,请确保查看文档,并找到用于访问原生输入组件的ref属性的prop。

如何使用React Hook Form进行表单验证

要对字段应用验证,可以将验证参数传递给register方法。验证参数类似于现有的HTML表单验证标准。

这些验证参数包括以下属性:

  • required:表示该字段是否为必填。如果此属性设置为true,则该字段不能为空。
  • minlengthmaxlength:设置字符串输入值的最小和最大长度。
  • minmax:设置数值的最小和最大值。
  • type:表示输入字段的类型;可以是emailnumbertext或其他标准HTML输入类型。
  • pattern:使用正则表达式定义输入值的模式。

如果您想将某个字段标记为 required,代码应该如下所示:

<input name="name" type="text" {...register('name', { required: true } )} />

现在尝试提交此字段为空的表单,这将导致以下错误对象:

{
  name: {
    type: "required",
    message: "",
    ref: <input name="name" type="text" />
  }
}

这里,type属性指的是验证失败的类型,ref属性包含原生DOM输入元素。

还可以通过向验证属性传递字符串而不是布尔值来包含自定义错误消息:

<form onSubmit={handleSubmit(handleRegistration, handleError)}>
  <div>
    <label>Name</label>
    <input name="name" {...register('name', { required: "Name is required" } )} />
  </div>
</form>

然后,通过使用useForm Hook访问errors对象:

const { register, handleSubmit, formState: { errors } } = useForm();

可以像这样向用户显示错误信息:

const RegisterForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const handleRegistration = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(handleRegistration)}>
      <div>
        <label>Name</label>
        <input type="text" name="name" {...register('name')} />
        {errors?.name && errors.name.message}
      </div>
      {/* more input fields... */}
      <button>Submit</button>
    </form>
  );
};

下面您可以找到完整的示例:

import React from "react";
import { useForm } from "react-hook-form";

const RegisterForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const handleRegistration = (data) => console.log(data);
  const handleError = (errors) => {};

  const registerOptions = {
    name: { required: "Name is required" },
    email: { required: "Email is required" },
    password: {
      required: "Password is required",
      minLength: {
        value: 8,
        message: "Password must have at least 8 characters"
      }
    }
  };

  return (
    <form onSubmit={handleSubmit(handleRegistration, handleError)}>
      <div>
        <label>Name</label>
        <input name="name" type="text" {...register('name', registerOptions.name) }/>
        <small className="text-danger">
          {errors?.name && errors.name.message}
        </small>
      </div>
      <div>
        <label>Email</label>
        <input
          type="email"
          name="email"
          {...register('email', registerOptions.email)}
        />
        <small className="text-danger">
          {errors?.email && errors.email.message}
        </small>
      </div>
      <div>
        <label>Password</label>
        <input
          type="password"
          name="password"
          {...register('password', registerOptions.password)}
        />
        <small className="text-danger">
          {errors?.password && errors.password.message}
        </small>
      </div>
      <button>Submit</button>
    </form>
  );
};
export default RegisterForm;

如果您希望在onChangeonBlur事件发生时验证字段,您可以将mode属性传递给useForm Hook:

const { register, handleSubmit, errors } = useForm({
  mode: "onBlur"
});

在API参考文档中可以找到有关useForm Hook的更多详细信息。

与第三方组件的使用

在某些情况下,你希望在表单中使用的外部UI组件可能不支持ref,只能通过状态进行控制。

React Hook Form为此类情况提供了相应的处理方法,并可以使用 Controller 组件轻松集成任何第三方受控组件。

React Hook Form提供了一个名为Controller的包装组件,允许您注册一个受控的外部组件,类似于register方法的工作方式。在这种情况下,您将使用control对象而不是register方法:

const { register, handleSubmit, control } = useForm();

假设您必须创建一个角色字段,表单将从一个select输入中接收值。可以使用react-select库来创建这个选择输入。

control对象应传递给Controller组件的control属性,并与字段的name属性一起使用。可以使用rules属性指定验证规则。

受控组件应该通过as属性传递给Controller组件。Select组件还需要一个options属性来渲染下拉选项:

<Controller
  name="role"
  control={control}
  defaultValue=""
  rules={registerOptions.role}
  render={({ field }) => (
    <Select options={selectOptions} {...field} label="Text field" />
  )}
/>

上述render属性为子组件提供onChangeonBlurnamerefvalue。通过将field展开传递给Select组件,React Hook Form注册了输入字段。

可以查看下面关于角色字段的完整示例:

import { useForm, Controller } from "react-hook-form";
import Select from "react-select";
// ...
const { register, handleSubmit, errors, control } = useForm({
  // 使用 mode 指定触发每个输入字段的事件
  mode: "onBlur"
});

const selectOptions = [
  { value: "student", label: "Student" },
  { value: "developer", label: "Developer" },
  { value: "manager", label: "Manager" }
];

const registerOptions = {
  // ...
  role: { required: "Role is required" }
};

// ...
<form>
  <div>
    <label>Your Role</label>
    <Controller
      name="role"
      control={control}
      defaultValue=""
      rules={registerOptions.role}
      render={({ field }) => (
        <Select options={selectOptions} {...field} label="Text field" />
      )}
    />
    <small className="text-danger">
      {errors?.role && errors.role.message}
    </small>
  </div>
</form>

还可以查看Controller组件的API参考文档以获得更详细的解释。

在React Hook Form中使用useFormContext

useFormContext 是React Hook Form提供的一个hook,允许你访问和操作深层嵌套组件的表单上下文/状态。它允许您在组件中共享表单方法,如 registererrorscontrol 等,而无需通过多层级传递 props

useFormContext 在需要访问深层嵌套组件中的表单方法或使用需要与表单状态交互的自定义hooks 时非常有用。以下是如何使用 useFormContext 的示例:

import React from 'react';
import { useForm, FormProvider, useFormContext } from 'react-hook-form';

const Input = ({ name }) => {
  const { register } = useFormContext();
  return <input {...register(name)} />;
};

const ContextForm = () => {
  const methods = useForm();
  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(data => console.log(data))}>
        <Input name="firstName" />
        <Input name="lastName" />
        <button type="submit">Submit</button>
      </form>
    </FormProvider>
  );
};

export default ContextForm;

在上面的示例中,Input组件使用useFormContext Hook来访问register方法,从而允许它注册输入字段,而无需从父组件逐层传递props。

还可以创建一个组件,使开发人员在处理更复杂的表单时更容易,例如当输入字段深层嵌套在组件树中时:

import { FormProvider, useForm, useFormContext } from "react-hook-form";

export const ConnectForm = ({ children }) => {
  const methods = useFormContext();
  return children({ ...methods });
};

export const DeepNest = () => (
  <ConnectForm>
    {({ register }) => <input {...register("hobbies")} />}
  </ConnectForm>
);

export const App = () => {
  const methods = useForm();

  return (
    <FormProvider {...methods}>
      <form>
        <DeepNest />
      </form>
    </FormProvider>
  );
};

处理数组和嵌套字段

React Hook Form 原生支持数组和嵌套字段,允许我们轻松处理复杂的数据结构。

要处理数组,可以使用 useFieldArray Hook。这是React Hook Form提供的一个自定义hook,用于帮助处理表单字段,如输入数组。该hook提供添加、删除和交换数组项的方法。我们看看useFieldArray Hook的实际操作:

import React from 'react';
import { useForm, FormProvider, useFieldArray, useFormContext } from 'react-hook-form';

const Hobbies = () => {
  const { control, register } = useFormContext();
  const { fields, append, remove } = useFieldArray({
    control,
    name: 'hobbies'
  });

  return (
    <div>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`hobbies.${index}.name`)} />
          <button type="button" onClick={() => remove(index)}>Remove</button>
        </div>
      ))}
      <button type="button" onClick={() => append({ name: '' })}>Add Hobby</button>
    </div>
  );
};

const MyForm = () => {
  const methods = useForm();

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

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <Hobbies />
        <button type="submit">Submit</button>
      </form>
    </FormProvider>
  );
};

export default MyForm;

在上述代码中,Hobbies组件使用useFieldArray来管理一个爱好数组。用户可以动态添加或删除爱好,并且每个爱好都有自己的一组字段。

还可以选择控制整个字段数组,以便在每个onChange事件发生时更新字段对象。可以将监视的字段数组值映射到受控字段,以确保输入更改反映在字段对象上:

import React from 'react';
import { useForm, FormProvider, useFieldArray, useFormContext } from 'react-hook-form';

const Hobbies = () => {
  const { control, register, watch } = useFormContext();
  const { fields, append, remove } = useFieldArray({
    control,
    name: 'hobbies'
  });
  const watchedHobbies = watch("hobbies");
  const controlledFields = fields.map((field, index) => ({
    ...field,
    ...watchedHobbies[index]
  }));

  return (
    <div>
      {controlledFields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`hobbies.${index}.name`)} defaultValue={field.name} />
          <button type="button" onClick={() => remove(index)}>Remove</button>
        </div>
      ))}
      <button type="button" onClick={() => append({ name: '' })}>Add Hobby</button>
    </div>
  );
};

const MyForm = () => {
  const methods = useForm({
    defaultValues: {
      hobbies: [{ name: "Reading" }]
    }
  });

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

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <Hobbies />
        <button type="submit">Submit</button>
      </form>
    </FormProvider>
  );
};

export default MyForm;

上面的代码使用watch函数来监视hobbies字段数组的变化,并使用controlledFields确保每个输入反映其最新状态。

嵌套字段可以类似于数组进行处理。您只需要在注册输入字段时使用点符号指定正确的路径:

import React from 'react';
import { useForm, FormProvider, useFormContext } from 'react-hook-form';

const Address = () => {
  const { register } = useFormContext();
  return (
    <div>
      <input {...register('address.street')} placeholder="Street" />
      <input {...register('address.city')} placeholder="City" />
    </div>
  );
};

const MyForm = () => {
  const methods = useForm();

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

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <Address />
        <button type="submit">Submit</button>
      </form>
    </FormProvider>
  );
};

export default MyForm;

上面的代码中,Address组件为地址对象中的streetcity字段注册了输入字段。这样,表单数据将被结构化为一个具有嵌套属性的对象:

{
  "address": {
    "street": "value",
    "city": "value"
  }
}

当使用useFormContext Hook时,如果管理不当,使用深层嵌套字段可能会影响应用程序的性能,因为FormProvider在表单状态更新时会触发重新渲染。使用React memo等工具可以帮助优化性能,通过防止不必要的重新渲染。

数组和嵌套字段的验证

React Hook Form支持使用Yup或Zod验证库对数组和嵌套字段进行验证。

以下示例使用Yup架构验证设置了对爱好数组和地址对象的验证。每个爱好名称和地址字段都根据指定的规则进行验证:

import React from 'react';
import { useForm, FormProvider, useFieldArray, useFormContext } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

const schema = yup.object().shape({
  hobbies: yup.array().of(
    yup.object().shape({
      name: yup.string().required('Hobby is required')
    })
  ),
  address: yup.object().shape({
    street: yup.string().required('Street is required'),
    city: yup.string().required('City is required')
  })
});

const Hobbies = () => {
  const { control, register } = useFormContext();
  const { fields, append, remove } = useFieldArray({
    control,
    name: 'hobbies'
  });

  return (
    <div>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`hobbies.${index}.name`)} placeholder="Hobby" />
          <button type="button" onClick={() => remove(index)}>Remove</button>
        </div>
      ))}
      <button type="button" onClick={() => append({ name: 'playing football' })}>Add Hobby</button>
    </div>
  );
};

const Address = () => {
  const { register } = useFormContext();
  return (
    <div>
      <input {...register('address.street')} placeholder="Street" />
      <input {...register('address.city')} placeholder="City" />
    </div>
  );
};

const App = () => {
  const methods = useForm({
    resolver: yupResolver(schema)
  });

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

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <Hobbies />
        <Address />
        <button type="submit">Submit</button>
      </form>
    </FormProvider>
  );
};

export default App;

结论

React Hook Form是React开源生态系统中的一个极好的补充,它大大简化了表单的创建和维护。其最大优势在于它专注于开发者体验并具有很强的灵活性。它可以与状态管理库无缝集成,并且在React Native中也能很好地工作。

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68k 声望104.9k 粉丝