You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript项目中Zod+react-hook-form表单输入错误提示干扰问题

问题:React Hook Form + Zod 验证错误提示干扰输入的解决方法

问题描述

使用React Hook Form构建注册表单,结合Zod实现验证逻辑,配置mode: "onSubmit"后,首次提交会正常显示错误,但提交后输入过程中错误提示会干扰输入:比如密码输入到8位、输入数字时会中断,邮箱输入也存在同类问题。调整useForm的不同模式及React状态变量均未解决。

代码复现

Register组件

import { useForm } from "react-hook-form";
import { z } from "zod";
import RegisterSchema from "../components/Authentication/Register/RegisterSchema";
import { AuthTitle } from "../components/Authentication";
import { zodResolver } from "@hookform/resolvers/zod";

type RegisterSchemaType = z.infer<typeof RegisterSchema>

type FormFieldProps = {
  labelText: string;
  inputText: string;
  registerInput: keyof RegisterSchemaType;
};

const Register = () => {

  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm<RegisterSchemaType>({ resolver: zodResolver(RegisterSchema), mode: "onSubmit"});

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

  console.log(errors);

  const FormFields = ({ labelText, inputText, registerInput } : FormFieldProps) => (
    <div className='flex flex-col'>
      <label className='uppercase text-[#49A078] text-sm sm:text-md' htmlFor={inputText}>
        {labelText}
      </label>
      <div className="flex flex-col relative">
        <input
          {...register(registerInput)}
          className='mb-1 mt-1 p-3 outline-1 outline-[#263228] outline rounded-sm pr-5 focus:outline-[#49A078] focus:outline-2'
        />
        {errors[registerInput] && (
          <span className="text-red-500 text-sm mt-0 mb-4">{errors[registerInput]?.message}</span>
        )}
      </div>
    </div>
  );

  return (
    <div className="flex flex-col h-screen justify-center items-center">
      <AuthTitle title="Trace" subTitle="Register" />

      <div className="flex md:flex-row flex-col">
        <form className="flex flex-col p-5 pr-8 border-r border-gray-400" onSubmit={handleSubmit(onSubmit)}>
          <FormFields labelText="First Name" inputText="firstName" registerInput="firstName"/>
          <FormFields labelText="Surname" inputText="surname" registerInput="surname"/>
          <FormFields labelText="Email Address" inputText="email" registerInput="email"/>
          <FormFields labelText="Password" inputText="password" registerInput="password"/>
          <button className="bg-[#263228] text-white p-3 rounded-sm uppercase mt-3" type="submit">
            Register
          </button>
        </form>

        {/* OAuth buttons */}
        <div className="flex items-center justify-center p-4">
          {/* Google OAuth */}
          {/* You may need to replace this with the appropriate library */}
          <h1>LOREM IPSUM</h1>

          {/* Apple OAuth */}
          {/* Add Apple OAuth button here */}
        </div>
      </div>

      {/* Bottom link */}
      <div className="mt-auto">
        <p className="text-center text-[#49A078] uppercase">Already have an account?</p>
      </div>
    </div>
  );
};

export default Register;

RegisterSchema

import { z } from "zod";

const RegisterSchema = z.object({
  email: z.string().email({message: "Invalid email."}),
  firstName: z.string().min(2, {message: "Must be 2 or more characters long."}),
  surname: z.string().min(2, {message: "Must be 2 or more characters long."}),
  password: z.string()
    .min(8, {message: "Must be 8 or more characters long."})
    .refine((password) => /\d/.test(password), {message: 'Must contain at least 1 number.',})
});

export default RegisterSchema;

问题根源

  1. 内部组件重复创建:FormFields组件定义在Register组件内部,每次Register因errors变化重渲染时,FormFields会被重新创建,导致输入框组件重新挂载,丢失焦点,表现为输入中断。
  2. 错误提示无限制显示:只要errors存在就显示错误提示,提交后输入过程中错误状态频繁变化,导致DOM频繁更新,进一步加剧焦点丢失问题。

解决方案

1. 将FormFields组件移至外部

把FormFields从Register函数内部提取为独立组件,避免每次父组件渲染时重新创建组件类型,防止输入框意外卸载重挂载。

2. 结合touched状态控制错误提示显示

从useForm的formState中解构出touched对象,仅当字段被触碰(提交后会自动标记所有字段为touched,用户点击输入框也会标记)且存在错误时,才显示错误提示,减少不必要的DOM更新。

3. 可选优化:调整验证配置

设置shouldFocusError: false,避免提交后自动聚焦错误字段(如果不需要该行为)。

修改后的代码

Register组件

import { useForm, UseFormRegister } from "react-hook-form";
import { z } from "zod";
import RegisterSchema from "../components/Authentication/Register/RegisterSchema";
import { AuthTitle } from "../components/Authentication";
import { zodResolver } from "@hookform/resolvers/zod";

type RegisterSchemaType = z.infer<typeof RegisterSchema>

type FormFieldProps = {
  labelText: string;
  inputText: string;
  registerInput: keyof RegisterSchemaType;
  error: string | undefined;
  isTouched: boolean;
  register: UseFormRegister<RegisterSchemaType>;
};

// 独立的FormFields组件
const FormFields = ({ labelText, inputText, registerInput, error, isTouched, register } : FormFieldProps) => (
  <div className='flex flex-col'>
    <label className='uppercase text-[#49A078] text-sm sm:text-md' htmlFor={inputText}>
      {labelText}
    </label>
    <div className="flex flex-col relative">
      <input
        {...register(registerInput)}
        className='mb-1 mt-1 p-3 outline-1 outline-[#263228] outline rounded-sm pr-5 focus:outline-[#49A078] focus:outline-2'
        id={inputText} // 关联label的for属性
      />
      {/* 仅在字段被触碰且有错误时显示提示 */}
      {isTouched && error && (
        <span className="text-red-500 text-sm mt-0 mb-4">{error}</span>
      )}
    </div>
  </div>
);

const Register = () => {

  const {
    register,
    handleSubmit,
    formState: { errors, touched }
  } = useForm<RegisterSchemaType>({ 
    resolver: zodResolver(RegisterSchema), 
    mode: "onSubmit",
    shouldFocusError: false // 可选:关闭提交后自动聚焦错误字段
  });

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

  return (
    <div className="flex flex-col h-screen justify-center items-center">
      <AuthTitle title="Trace" subTitle="Register" />

      <div className="flex md:flex-row flex-col">
        <form className="flex flex-col p-5 pr-8 border-r border-gray-400" onSubmit={handleSubmit(onSubmit)}>
          <FormFields 
            labelText="First Name" 
            inputText="firstName" 
            registerInput="firstName"
            error={errors.firstName?.message}
            isTouched={!!touched.firstName}
            register={register}
          />
          <FormFields 
            labelText="Surname" 
            inputText="surname" 
            registerInput="surname"
            error={errors.surname?.message}
            isTouched={!!touched.surname}
            register={register}
          />
          <FormFields 
            labelText="Email Address" 
            inputText="email" 
            registerInput="email"
            error={errors.email?.message}
            isTouched={!!touched.email}
            register={register}
          />
          <FormFields 
            labelText="Password" 
            inputText="password" 
            registerInput="password"
            error={errors.password?.message}
            isTouched={!!touched.password}
            register={register}
          />
          <button className="bg-[#263228] text-white p-3 rounded-sm uppercase mt-3" type="submit">
            Register
          </button>
        </form>

        {/* OAuth buttons */}
        <div className="flex items-center justify-center p-4">
          {/* Google OAuth */}
          {/* You may need to replace this with the appropriate library */}
          <h1>LOREM IPSUM</h1>

          {/* Apple OAuth */}
          {/* Add Apple OAuth button here */}
        </div>
      </div>

      {/* Bottom link */}
      <div className="mt-auto">
        <p className="text-center text-[#49A078] uppercase">Already have an account?</p>
      </div>
    </div>
  );
};

export default Register;

关键修改说明

  • 组件独立化:FormFields移到外部后,不会随Register重渲染而重新创建,输入框的状态和焦点得以保留。
  • 错误提示条件化:通过touched状态过滤错误显示时机,减少DOM更新频率,避免因提示显示/隐藏导致的布局变化干扰输入。
  • 可访问性优化:补充input的id属性,与label的for属性关联,符合表单可访问性标准。

内容的提问来源于stack exchange,提问作者Jexarx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 05:27:05