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

React Hook Form条件渲染表单字段提交出错时无法获焦的问题

解决React Hook Form中条件渲染字段的错误焦点优先级问题

问题场景

表单提交出错时,希望条件渲染的表单字段能优先获取焦点。比如条件渲染的字段A位于始终渲染的字段B上方,若A存在错误,提交后A应比B先获得焦点,但目前React Hook Form(RHF)会跳过这类条件渲染且有错误的字段的焦点设置。

相关代码

表单校验Schema(Zod)

const FormSchema = z.object({
  nameOfBusiness: z
    .string()
    .min(1, { message: "请填写内容" })
    .max(260, { message: "名称过长" }),
  modeOfPayment: z.string().nonempty({ message: "请选择付款方式" }),
  tillNumber: z
    .string()
    .nonempty({ message: "请填写内容" })
    .min(5, { message: "请输入有效的收银号" })
    .max(8, { message: "请输入有效的收银号" }),
  //... 其他字段
});

表单组件

<div>
  <div>
    <h2>付款方式</h2>
    {/* 父字段 - 非条件渲染,出错时能正确获得焦点优先级 */}
    <select
      {...register("modeOfPayment", {
        onChange: (e) => {
          setSelectedPaymentMode(e.target.value);
        },
      })}
      {/* ... 其他属性 */}
    >
      <option value="" disabled={true}>选择付款方式</option>
      <option>Mpesa 商品购买</option>
      <option>Mpesa 账单支付</option>
    </select>
    {errors?.modeOfPayment?.message && (
      <p className="text-red-700 mt-1">{errors.modeOfPayment.message}</p>
    )}
  </div>

  {/* 子字段 - 条件渲染,出错时无法获得焦点优先级 */}
  {selectedPaymentMode === "Mpesa 商品购买" && (
    <div>
      <h2>
        输入收银号
        <span className="text-red-700 ml-1">*</span>
      </h2>
      <input
        type="text"
        {...register("tillNumber")}
        {/* ... 其他属性 */}
      />
      {errors?.tillNumber?.message && (
        <p className="text-red-700 mt-1">{errors.tillNumber.message}</p>
      )}
    </div>
  )}

  {/* 其他字段... */}
  <div>
    <h2>商家名称</h2>
    {/* 非条件渲染,错误时错误地优先于上方条件渲染字段获得焦点 */}
    <input
      type="text"
      {...register("nameOfBusiness")}
      {/* ... 其他属性 */}
    />
    {errors?.nameOfBusiness?.message && (
      <p className=" text-red-700 mt-1 pl-4 lg:pl-8 min-[400px]:pl-6">
        {errors.nameOfBusiness.message}
      </p>
    )}
  </div>
</div>

解决方案

RHF默认焦点逻辑会忽略未挂载的字段,需要手动监听表单错误状态,找到第一个存在错误且已渲染的字段,主动设置焦点:

实现代码

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import { useEffect, useRef, useState } from "react";

const FormSchema = z.object({
  nameOfBusiness: z
    .string()
    .min(1, { message: "请填写内容" })
    .max(260, { message: "名称过长" }),
  modeOfPayment: z.string().nonempty({ message: "请选择付款方式" }),
  tillNumber: z
    .string()
    .nonempty({ message: "请填写内容" })
    .min(5, { message: "请输入有效的收银号" })
    .max(8, { message: "请输入有效的收银号" }),
});

export default function PaymentForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(FormSchema),
  });
  const [selectedPaymentMode, setSelectedPaymentMode] = useState("");
  const tillNumberRef = useRef(null);

  // 监听错误和付款方式变化,设置焦点
  useEffect(() => {
    // 按表单视觉顺序定义字段优先级
    const fieldOrder = ["modeOfPayment", "tillNumber", "nameOfBusiness"];
    
    const firstErrorField = fieldOrder.find(fieldName => {
      if (!errors[fieldName]) return false;
      // 针对条件渲染字段,判断是否已挂载
      if (fieldName === "tillNumber") {
        return selectedPaymentMode === "Mpesa 商品购买";
      }
      // 非条件字段默认已渲染
      return true;
    });

    if (firstErrorField) {
      let element;
      if (firstErrorField === "tillNumber") {
        element = tillNumberRef.current;
      } else {
        element = document.querySelector(`[name="${firstErrorField}"]`);
      }
      element?.focus();
    }
  }, [errors, selectedPaymentMode]);

  const onSubmit = (data) => {
    console.log(data);
    // 此处添加表单提交逻辑
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <h2>付款方式</h2>
        <select
          {...register("modeOfPayment", {
            onChange: (e) => setSelectedPaymentMode(e.target.value),
          })}
        >
          <option value="" disabled={true}>选择付款方式</option>
          <option>Mpesa 商品购买</option>
          <option>Mpesa 账单支付</option>
        </select>
        {errors?.modeOfPayment?.message && (
          <p className="text-red-700 mt-1">{errors.modeOfPayment.message}</p>
        )}
      </div>

      {selectedPaymentMode === "Mpesa 商品购买" && (
        <div>
          <h2>
            输入收银号
            <span className="text-red-700 ml-1">*</span>
          </h2>
          <input
            type="text"
            ref={tillNumberRef}
            {...register("tillNumber")}
          />
          {errors?.tillNumber?.message && (
            <p className="text-red-700 mt-1">{errors.tillNumber.message}</p>
          )}
        </div>
      )}

      <div>
        <h2>商家名称</h2>
        <input
          type="text"
          {...register("nameOfBusiness")}
        />
        {errors?.nameOfBusiness?.message && (
          <p className=" text-red-700 mt-1 pl-4 lg:pl-8 min-[400px]:pl-6">
            {errors.nameOfBusiness.message}
          </p>
        )}
      </div>

      <button type="submit">提交表单</button>
    </form>
  );
}

关键说明

  • 用fieldOrder数组明确表单字段的视觉顺序,确保焦点优先级符合用户预期
  • 针对条件渲染字段,额外判断其当前是否处于渲染状态
  • 通过ref或DOM查询获取目标字段元素,调用focus()方法手动设置焦点

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 12:24:54