React函数组件中Hook Form+Zod更新表单验证多次提交异常
问题描述
本人拥有20年JavaScript和C#开发经验,但刚接触React。使用hook-form、react-bootstrap、zod和flowbite-react实现了创建与更新两个近乎相同的表单,创建表单验证正常,但更新表单的必填输入项验证存在异常:
- 首次点击提交按钮时,两个必填项均出现验证错误;
- 第二次提交仅第二个必填项报错;
- 第三次提交才成功将更新数据发送至服务器,且提交时两个输入框均已填写内容。
相关组件代码、Zod验证Schema如下:
更新表单组件代码
export const MembershipUpdate = () => { const [membership, setMembership] = useState( { id: 0, firstName: '', lastName: '', fullName: '', shortname: '', nickName: '', wheelchair: false } ); const location = useLocation(); const id: number = location.state; const { register, handleSubmit, formState: { errors }, } = useForm<UpdateFormData>({ resolver: zodResolver(UpdateFormDataSchema), }); const onSubmit: SubmitHandler<UpdateFormData> = (data) =>updateData(data) // send updated record to server const navigate = useNavigate(); function OnChange(e: React.ChangeEvent<HTMLInputElement>) { setMembership({ ...membership, [e.target.name]: e.target.value }); } function OnChecked(e: React.ChangeEvent<HTMLInputElement>) { setMembership({ ...membership, [e.target.name]: e.target.checked }); } useEffect(() => { GetData(); // get original record from server }, []); const contents = membership === undefined ? <p><em>Loading ...</em></p> : <form onSubmit={handleSubmit(onSubmit)} > <input type="hidden" {...register("id", { valueAsNumber: true })} value={membership.id} /> <TextInput {...register("nickName")} type="hidden" /> <TextInput {...register("fullName")} type="hidden" /> <Container > <Row> <Col style={{ width: '15%' }}><Label>First Name:</Label></Col> <Col><TextInput type="text" {...register('firstName')} style={{ width: '85%' }} value={membership.firstName} onChange={OnChange} /></Col> </Row> <Row> <Col style={{ width: '15%' }}><Label>Last Name:</Label></Col> <Col><TextInput {...register('lastName')} style={{ width: '85%' }} value={membership.lastName} onChange={OnChange} /></Col> </Row> <Row> <Col style={{ width: '15%' }}><Label>Short Name:</Label></Col> <Col><TextInput {...register('shortname')} style={{ width: '85%' }} value={membership.shortname} onChange={OnChange} /></Col> </Row> <Row> <Col style={{ width: '15%' }}><Label>Wheel Chair:</Label></Col> <Col style={{ textAlign: "left", width: '85%' }}> <Checkbox {...register('wheelchair')}checked={membership.wheelchair} onChange={OnChecked} /></Col> </Row> <Row> <Col style={{ textAlign: "center", width: '100%' }}> <TextInput type="submit" /> </Col> </Row> {errors.firstName && <p className="errorMessage">{errors.firstName.message}</p>} {errors.lastName && <p className="errorMessage">{errors.lastName.message}</p>} {errors.shortname && <p className="errorMessage">{errors.shortname.message}</p>} </Container> </form> return ( <> <h1>Update record</h1> {contents} </> ); }
Zod验证Schema
import { z } from 'zod'; export const UpdateFormDataSchema = z.object({ id: z.number(), firstName: z.string().min(1, "First Name is required").max(50, "First Nmme has a maximum of 50 characters"), lastName: z.string().min(1, "Last Name is required").max(50, "Last Nmme has a maximum of 50 characters"), fullName: z.string(), // computed column in DB shortname: z.string().max(25, "Short Nmme has a maximum of 25 characters"), nickName: z.string(), //computed column in DB wheelchair: z.boolean() }); export type UpdateFormData = z.infer<typeof UpdateFormDataSchema>;
问题根源与解决方案
核心问题
同时维护了本地state(membership)和react-hook-form内部表单状态,导致两者数据不同步。react-hook-form的设计理念就是接管表单状态管理,手动设置value和onChange会干扰它的验证逻辑,引发提交时的状态不一致问题。
修复步骤
- 移除本地state和自定义变更处理函数:删掉
useState、OnChange、OnChecked,让react-hook-form完全管理表单数据。 - 初始化表单默认值:在
useForm中通过defaultValues设置初始值,后续用reset方法同步服务器返回的数据。 - 移除输入框的value和onChange属性:
register方法已经包含了必要的绑定,不需要额外指定。
修改后的代码示例
export const MembershipUpdate = () => { const location = useLocation(); const id: number = location.state; const { register, handleSubmit, formState: { errors }, reset // 获取reset方法用于同步服务器数据 } = useForm<UpdateFormData>({ resolver: zodResolver(UpdateFormDataSchema), defaultValues: { // 设置初始默认值 id: 0, firstName: '', lastName: '', fullName: '', shortname: '', nickName: '', wheelchair: false } }); const onSubmit: SubmitHandler<UpdateFormData> = (data) => updateData(data); const navigate = useNavigate(); useEffect(() => { // 获取服务器数据后,用reset同步到表单 const fetchData = async () => { const data = await GetData(id); // 假设GetData接受id参数并返回数据 reset(data); // 将服务器数据设置为表单当前值 }; fetchData(); }, [id, reset]); const contents = ( <form onSubmit={handleSubmit(onSubmit)} > <input type="hidden" {...register("id", { valueAsNumber: true })} /> <TextInput {...register("nickName")} type="hidden" /> <TextInput {...register("fullName")} type="hidden" /> <Container > <Row> <Col style={{ width: '15%' }}><Label>First Name:</Label></Col> <Col><TextInput type="text" {...register('firstName')} style={{ width: '85%' }} /></Col> </Row> <Row> <Col style={{ width: '15%' }}><Label>Last Name:</Label></Col> <Col><TextInput {...register('lastName')} style={{ width: '85%' }} /></Col> </Row> <Row> <Col style={{ width: '15%' }}><Label>Short Name:</Label></Col> <Col><TextInput {...register('shortname')} style={{ width: '85%' }} /></Col> </Row> <Row> <Col style={{ width: '15%' }}><Label>Wheel Chair:</Label></Col> <Col style={{ textAlign: "left", width: '85%' }}> <Checkbox {...register('wheelchair')} /></Col> </Row> <Row> <Col style={{ textAlign: "center", width: '100%' }}> <TextInput type="submit" /> </Col> </Row> {errors.firstName && <p className="errorMessage">{errors.firstName.message}</p>} {errors.lastName && <p className="errorMessage">{errors.lastName.message}</p>} {errors.shortname && <p className="errorMessage">{errors.shortname.message}</p>} </Container> </form> ) return ( <> <h1>Update record</h1> {contents} </> ); }
额外提示
Schema里的错误提示有拼写错误(First Nmme、Last Nmme),建议修正为First Name、Last Name,避免用户看到错误的提示文本。
内容的提问来源于stack exchange,提问作者Jeff
相关产品推荐
相关产品推荐

