在本教程中,您将学习如何使用 react-hook-form 和 zod 构建带有验证的表单。

如果您更喜欢视频教程,可以在下面观看。

从 GitHub克隆项目。

这就是我们要构建的:

[反应表单与验证](https://res.cloudinary.com/practicaldev/image/fetch/s--ZLLeBXtO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev- to-uploads.s3.amazonaws.com/uploads/articles/1gkxlce0vw71xm65csfh.jpg)

让我们从表单组件的一些样板代码开始:

import { FC } from "react";

const Tiers = [
  {
    id: "BRONZE",
    name: "Bronze",
    description: "Get average points",
    price: 0.99,
  },
  {
    id: "SILVER",
    name: "Silver",
    description: "Get extra points",
    price: 4.99,
  },
  {
    id: "GOLD",
    name: "Gold",
    description: "The highest possible tier",
    price: 19.99,
  },
];

export const Form: FC = () => {

  return (
    <form className="space-y-10">
      <div>
        <label className="block">
          <span className="block">Email</span>
          <input
            type="text"
            className={`block border text-lg px-4 py-3 mt-2 rounded-lg border-gray-200 focus:bg-white text-gray-900 focus:border-blue-600 focus:ring-0 outline-none w-full  disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed`}
          />
        </label>
      </div>
      <div>
        <label className="flex items-center">
          <input
            type="checkbox"
            className="block border text-lg rounded w-6 h-6 border-gray-200 text-blue-600 focus:ring-0 focus:outline-none focus:ring-offset-0 disabled:text-gray-200 disabled:cursor-not-allowed"
          />
          <span className="block ml-4">I accept the Terms of Service</span>
        </label>
      </div>
      <div>
        <p className="block">Payment Tier</p>
        <ul className="space-y-2 mt-2">
          {Tiers.map((tier) => {
            return (
              <li
                className={`border rounded-lg border-gray-200 text-gray-900`}
                key={tier.id}
              >
                <label
                  className={`flex justify-between px-6 py-4 items-center cursor-pointer`}
                >
                  <div>
                    <p className={`font-medium text-lg`}>{tier.name}</p>
                    <p className={`text-sm opacity-80`}>{tier.description}</p>
                  </div>
                  <div className="flex items-center">
                    <p className={`font-medium mr-4 text-sm`}>
                      {tier.price.toLocaleString("en-US", {
                        currency: "USD",
                        style: "currency",
                      })}
                    </p>
                    <input
                      type="radio"
                      className="w-6 h-6 border ring-0 border-gray-200 text-blue-600 disabled:text-gray-300 outline-none focus:ring-0 focus:ring-offset-0 cursor-pointer"
                      value={tier.id}
                    />
                  </div>
                </label>
              </li>
            );
          })}
        </ul>
      </div>
      <button
        type="submit"
        className="w-full px-8 py-4 flex items-center justify-center uppercase text-white font-semibold bg-blue-600 rounded-lg disabled:bg-gray-100 disabled:text-gray-400"
      >
        Create account
      </button>
    </form>
  );
};

进入全屏模式 退出全屏模式

这只是为我们提供了带有样式的表单,还没有添加任何功能。

使用 zod 构建表单验证模式

让我们构建一个与表单中的值匹配的模式。

让我们从导入必要的库开始:

import { z } from "zod";
import { SubmitHandler, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

进入全屏模式 退出全屏模式

让我们用一些自定义错误消息定义我们的模式:

const FormSchema = z.object({
  email: z.string().email(),
  accept: z.literal(true, {
    invalid_type_error: "You must accept Terms and Conditions.",
  }),
  tier: z
    .string({ invalid_type_error: "Please select a payment tier." })
    .refine((val) => Tiers.map((tier) => tier.id).includes(val)),
});

进入全屏模式 退出全屏模式

我们将为我们的电子邮件字段使用与电子邮件验证链接的字符串验证。

对于接受服务条款复选框,我们将使用值为 true 的文字验证器。字面量只是意味着该字段必须正是这个值。请注意,我们还为 invalid_type_error 使用自定义错误消息。在本教程的后面,您将学习如何显示错误消息。

对于我们的支付层验证,我们首先检查该值是否为字符串,然后使用自定义验证使用细化来检查该字符串是否与我们预定义的 Tiers 数组中的一个 ID 匹配。

让我们从中推断出我们将要使用的类型:

type FormSchemaType = z.infer<typeof FormSchema>;

进入全屏模式 退出全屏模式

我们可以看到 TypeScript 从中推断出以下类型:

type FormSchemaType = {
    email: string;
    accept: true;
    tier: string;
}

进入全屏模式 退出全屏模式

这将帮助我们保持所有函数类型的安全。

使用 react-hook-form

让我们使用 react-hook-form 来处理我们的表单状态。

在您的表单组件中添加此代码:

export const Form: FC = () => {
  const {
    register,
    watch,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormSchemaType>({
    resolver: zodResolver(FormSchema),
  });

  const onSubmit: SubmitHandler<FormSchemaType> = async (data) => {
    await new Promise(async (resolve) => {
      await setTimeout(() => {
        console.log(data);
        resolve(undefined);
      }, 3000);
    });
  };

进入全屏模式 退出全屏模式

我们已经使用了useForm函数并给它我们模式的类型。这将有助于 TypeScript 正确地保持我们的代码类型安全。

我们创建了一个onSubmit函数,它会在 3 秒延迟后将经过验证的表单数据记录到控制台中。我想添加一个人为的延迟来更好地模拟真实世界的场景。

如果我们尝试填写表单并提交它,什么也不会发生。这是因为我们还没有注册表单输入或让表单使用我们自定义的onSubmit函数。

注册输入

我们可以使用从useForm获得的注册函数注册表单输入,方法是提供与我们模式中的字段匹配的字段名称。

例如对于电子邮件字段:

          <input
            type="text"
            className={`block border text-lg px-4 py-3 mt-2 rounded-lg border-gray-200 focus:bg-white text-gray-900 focus:border-blue-600 focus:ring-0 outline-none w-full  disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed`}
            {...register("email")}
          />

进入全屏模式 退出全屏模式

并接受服务条款:

          <input
            type="checkbox"
            className="block border text-lg rounded w-6 h-6 border-gray-200 text-blue-600 focus:ring-0 focus:outline-none focus:ring-offset-0 disabled:text-gray-200 disabled:cursor-not-allowed"
            {...register("accept")}
          />

进入全屏模式 退出全屏模式

对于支付层单选按钮:

                    <input
                      type="radio"
                      className="w-6 h-6 border ring-0 border-gray-200 text-blue-600 disabled:text-gray-300 outline-none focus:ring-0 focus:ring-offset-0 cursor-pointer"
                      value={tier.id}
                      {...register("tier")}
                    />

进入全屏模式 退出全屏模式

使用自定义 onSubmit 处理程序

我们从useForm得到的handleSubmit函数做了两件事。首先它禁用任何默认的表单提交行为,其次它使用经过验证的数据调用我们的自定义onSubmit函数。

    <form className="space-y-10" onSubmit={handleSubmit(onSubmit)}>

进入全屏模式 退出全屏模式

现在,如果您尝试填写表单并提交它,您将看到 3 秒后经过验证的表单值被记录到控制台中。

如果您使用无效值填写表单,您将看到出现正确的错误消息。

您可能注意到的一个问题是您可以多次单击创建帐户按钮,并且表单将提交多次。这显然是我们不希望发生的事情。

让我们通过在提交表单时禁用所有表单输入和提交按钮来解决这个问题。

禁用表单输入

我们将使用从formState中获得的isSubmitting值(从useForm中获得)来检查表单当前是否正在提交。

对于我们的输入和提交按钮,我们将使用该值禁用它们。

我们的电子邮件输入示例:

          <input
            type="text"
            className={`block border text-lg px-4 py-3 mt-2 rounded-lg border-gray-200 focus:bg-white text-gray-900 focus:border-blue-600 focus:ring-0 outline-none w-full  disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed`}
            {...register("email")}
            disabled={isSubmitting}
          />

进入全屏模式 退出全屏模式

disabled={isSubmitting}属性添加到其他字段和提交按钮。

现在,当您提交表单时,您会注意到所有字段和提交按钮都被禁用,直到数据被记录到控制台中。

但是如果表格无效怎么办?

显示错误信息

目前,如果您尝试提交包含无效字段的表单,则不会发生任何事情。

让我们通过有条件地显示每个字段的错误消息来改变它,如果它们是无效的。

对于我们的电子邮件字段:

          <input
            type="text"
            className={`block border text-lg px-4 py-3 mt-2 rounded-lg border-gray-200 focus:bg-white text-gray-900 focus:border-blue-600 focus:ring-0 outline-none w-full  disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed`}
            {...register("email")}
            disabled={isSubmitting}
          />
        </label>
        {errors.email && (
          <p className="text-sm text-red-600 mt-1">{errors.email.message}</p>
        )}

进入全屏模式 退出全屏模式

并接受服务条款按钮:

        {errors.accept && (
          <p className="text-sm text-red-600 mt-1">{errors.accept.message}</p>
        )}

进入全屏模式 退出全屏模式

对于ul标签之后的付款层级:

        {errors.tier && (
          <p className="text-sm text-red-600 mt-1">{errors.tier.message}</p>
        )}

进入全屏模式 退出全屏模式

现在,当您尝试提交包含无效字段的表单时,您应该会看到显示的错误消息。

react-hook-form 的默认行为是在第一次提交时验证表单。在此之后,它将在每次按键和模糊事件后验证表单。

结论

在本教程中,您学习了如何结合 react-hook-form 和 zod 来创建具有验证功能的完整表单。

对于后续步骤,请深入了解react-hook-form 文档以了解更高级的概念,例如:动态生成的字段和多步表单。

Logo

React社区为您提供最前沿的新闻资讯和知识内容

更多推荐