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

Zod+React Hook Form多字段联动验证的错误显示与清除问题

Zod + React Hook Form 跨字段校验:双字段错误同步显示与清除

问题分析

你遇到的两个核心问题:

  • refine多路径失效:Zod的refine配置中path仅支持单个字段路径,传入数组会导致解析异常,因此错误仅在第一个字段显示且信息变为unknown。
  • superRefine错误未同步清除:React Hook Form默认仅校验用户修改过的「脏字段」,当条件满足时,未被编辑的字段不会自动重新校验,错误残留。

解决方案

1. 保留superRefine实现双字段错误标记

superRefine是给多个字段添加自定义错误的正确方式,继续使用这个逻辑确保错误信息显示在fieldTwo和fieldThree上。

2. 强制同步校验两个字段

通过监听三个字段的变化,任一字段更新时触发fieldTwo和fieldThree的校验,确保条件满足时两个字段的错误同时清除。以下是两种可行实现方式:

方式一:字段onChange时触发校验

在fieldTwo和fieldThree的onChange回调中手动触发对方字段的校验,同时监听fieldOne的变化触发两个字段的校验。

方式二:watch+useEffect批量触发校验

通过watch监听三个字段的值变化,任一值改变时批量触发fieldTwo和fieldThree的校验,代码更简洁。

完整修改代码

以下是watch+useEffect的实现示例:

import { Button } from "@/components/ui/button";
import { DialogFooter } from "@/components/ui/dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useEffect } from "react";

const Test = () => {
  const schema = z
    .object({
      fieldOne: z.coerce.number(),
      fieldTwo: z.coerce.number(),
      fieldThree: z.coerce.number(),
    })
    .superRefine((args, ctx) => {
      const sum = args.fieldTwo + args.fieldThree;
      if (sum !== args.fieldOne) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path: ["fieldTwo"],
          message: "fieldTwo + fieldThree must equal fieldOne",
        });
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path: ["fieldThree"],
          message: "fieldTwo + fieldThree must equal fieldOne",
        });
      }
    });

  const form = useForm<z.infer<typeof schema>>({
    resolver: zodResolver(schema),
  });

  // 监听三个字段变化,同步触发fieldTwo和fieldThree的校验
  const fieldOne = form.watch("fieldOne");
  const fieldTwo = form.watch("fieldTwo");
  const fieldThree = form.watch("fieldThree");

  useEffect(() => {
    form.trigger(["fieldTwo", "fieldThree"]);
  }, [fieldOne, fieldTwo, fieldThree, form]);

  const onSubmit = (values: z.infer<typeof schema>) => {
    console.log("SUBMIT::", values);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="fieldOne"
          render={({ field }) => (
            <FormItem className="grid grid-cols-4 items-center !mt-2 gap-4">
              <FormLabel className="text-right">FieldOne</FormLabel>
              <div className="col-span-3">
                <FormControl>
                  <Input placeholder="Enter fieldOne" {...field} />
                </FormControl>
                <FormMessage className="mt-2" />
              </div>
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="fieldTwo"
          render={({ field }) => (
            <FormItem className="grid grid-cols-4 items-center !mt-2 gap-4">
              <FormLabel className="text-right">FieldTwo</FormLabel>
              <div className="col-span-3">
                <FormControl>
                  <Input placeholder="Enter fieldTwo" {...field} />
                </FormControl>
                <FormMessage className="mt-2" />
              </div>
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="fieldThree"
          render={({ field }) => (
            <FormItem className="grid grid-cols-4 items-center !mt-2 gap-4">
              <FormLabel className="text-right">FieldThree</FormLabel>
              <div className="col-span-3">
                <FormControl>
                  <Input placeholder="Enter fieldThree" {...field} />
                </FormControl>
                <FormMessage className="mt-2" />
              </div>
            </FormItem>
          )}
        />
        <DialogFooter>
          <Button type="submit" className="w-1/4">
            Submit
          </Button>
        </DialogFooter>
      </form>
    </Form>
  );
};

export default Test;

关键说明

  • superRefine逻辑不变:当和不等于fieldOne时,给两个字段添加自定义错误。
  • useEffect监听字段变化:只要fieldOne、fieldTwo、fieldThree任一值改变,就触发两个字段的校验,确保错误状态同步更新。
  • 替代方案:若不想用useEffect,可在每个字段的onChange中添加触发逻辑,例如fieldTwo的onChange改为:
    onChange={(e) => {
      field.onChange(e);
      form.trigger("fieldThree");
    }}
    
    同时给fieldOne和fieldThree做类似处理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 06:05:02