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;
问题根源
- 内部组件重复创建:
FormFields组件定义在Register组件内部,每次Register因errors变化重渲染时,FormFields会被重新创建,导致输入框组件重新挂载,丢失焦点,表现为输入中断。 - 错误提示无限制显示:只要
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
相关产品推荐
相关产品推荐

