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

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会干扰它的验证逻辑,引发提交时的状态不一致问题。

修复步骤

  1. 移除本地state和自定义变更处理函数:删掉useState、OnChange、OnChecked,让react-hook-form完全管理表单数据。
  2. 初始化表单默认值:在useForm中通过defaultValues设置初始值,后续用reset方法同步服务器返回的数据。
  3. 移除输入框的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 18:35:01