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
相关产品推荐
相关产品推荐

