如何在React Hook Form中验证多步骤表单的指定区块?
问题:React Hook Form 向导式表单中验证指定区块有效性
表单结构
{ "contactInfo": { "name": "string", "email": "string", ... }, "petInfo": { "petName": "string", "petBreed": "string", ... }, "vetInfo": { "vetName": "string", "vetPhone": "string", ... } }
组件代码
// Form Validation import { yupResolver } from "@hookform/resolvers/yup"; import schema from "utils/FormValidation/wizardFormValidation"; import { defaultValues } from "utils/FormValidation/packFormValidation"; function getSteps(): string[] { return ["Contact", "Emergency", "Pack", "Vet"]; } function getStepContent(stepIndex: number): JSX.Element { switch (stepIndex) { case 0: return <Contact />; case 1: return <Emergency />; case 2: return <Pack />; case 3: return <Vet />; default: return null; } } function Welcome(): JSX.Element { const dispatch = useAppDispatch(); const petParent = useAppSelector(selectPetParent); const pack = useAppSelector(selectPack); const wizard = useAppSelector((state) => state.forms.wizard); const values = { ...petParent, pack: pack.length !== 0 ? [...pack] : [...defaultValues], } as Wizard; const [activeStep, setActiveStep] = useState<number>(0); const steps = getSteps(); const isLastStep: boolean = activeStep === steps.length - 1; const handleNext = () => setActiveStep(activeStep + 1); const handleBack = () => setActiveStep(activeStep - 1); const methods = useForm({ mode: "all", resolver: yupResolver(schema), defaultValues: { ...wizard, }, values: values, }); const onSubmit = () => { const formValues: Wizard = methods.getValues() as unknown as Wizard; dispatch(submitWizardForm(formValues)).unwrap(); }; const handleDisabled = (): boolean => { let section: any; switch (activeStep) { case 0: section = "contactInfo"; break; case 1: section = "eContacts"; break; case 2: section = "pack"; break; case 3: section = "vetInfo"; break; } const fieldState = methods.getFieldState(section, methods.formState); return activeStep !== 3 ? fieldState.invalid : !methods.formState.isValid; }; useEffect(() => { const subscription = methods.watch((value, { name, type }) => { dispatch(updateWizardForm(value)); }); return () => subscription.unsubscribe(); }, [methods.watch]); return ( <FormProvider {...methods}> <div role="form" onSubmit={methods.handleSubmit(onSubmit)}> <div>{getStepContent(activeStep)}</div> {activeStep === 0 ? ( <div></div> ) : ( <button onClick={handleBack}>back</button> )} <button disabled={handleDisabled()} onClick={!isLastStep ? handleNext : onSubmit} > {isLastStep ? "submit" : "next"} </button> </div> </FormProvider> ); } export default Welcome;
问题描述
需要判断每个表单区块(contactInfo、petInfo、vetInfo等)是否有效,以此控制「下一步」按钮的禁用状态。目前通过getFieldState获取区块的invalid状态,但区块为空时该状态不总是返回false;尝试isTouched和isDirty也无法覆盖区块内所有字段状态,导致用户无法明确需要补全的字段。希望在不拆分表单的前提下,检查指定区块的有效性,而非逐个字段验证。
解决方案
方案1:利用Yup子Schema单独验证区块
如果表单验证基于Yup构建,可将完整Schema拆分为各区块的子Schema,单独验证当前步骤对应的区块:
- 拆分Yup Schema
在wizardFormValidation.ts中导出各区块子Schema:
// utils/FormValidation/wizardFormValidation.ts import * as yup from "yup"; // 各区块子Schema export const contactInfoSchema = yup.object({ name: yup.string().required(), email: yup.string().email().required(), // ...其他字段 }); export const eContactsSchema = yup.object({ // Emergency区块字段验证规则 }); export const packSchema = yup.object({ // Pack区块字段验证规则 }); export const vetInfoSchema = yup.object({ vetName: yup.string().required(), vetPhone: yup.string().required(), // ...其他字段 }); // 完整表单Schema const schema = yup.object({ contactInfo: contactInfoSchema, eContacts: eContactsSchema, pack: packSchema, vetInfo: vetInfoSchema, }); export default schema;
- 修改组件验证逻辑
导入子Schema,异步验证当前区块并维护有效性状态:
import { contactInfoSchema, eContactsSchema, packSchema, vetInfoSchema } from "utils/FormValidation/wizardFormValidation"; function Welcome(): JSX.Element { // ...其他状态和逻辑 const [isStepValid, setIsStepValid] = useState<boolean>(false); // 获取当前步骤对应的区块名 const getCurrentSection = (step: number): string => { switch (step) { case 0: return "contactInfo"; case 1: return "eContacts"; case 2: return "pack"; case 3: return "vetInfo"; default: return ""; } }; // 验证当前步骤区块有效性 const checkStepValidity = async () => { const currentValues = methods.getValues(); const currentStep = activeStep; let sectionSchema; switch (currentStep) { case 0: sectionSchema = contactInfoSchema; break; case 1: sectionSchema = eContactsSchema; break; case 2: sectionSchema = packSchema; break; case 3: sectionSchema = vetInfoSchema; break; default: return setIsStepValid(false); } if (currentStep === 3) { return setIsStepValid(methods.formState.isValid); } try { await sectionSchema.validate(currentValues[getCurrentSection(currentStep)]); setIsStepValid(true); } catch (err) { setIsStepValid(false); } }; // 步骤切换时重新验证 useEffect(() => { checkStepValidity(); }, [activeStep, methods.getValues]); // 当前步骤字段变化时重新验证 useEffect(() => { const subscription = methods.watch((_, { name }) => { const currentSection = getCurrentSection(activeStep); if (name.startsWith(currentSection)) { checkStepValidity(); } }); return () => subscription.unsubscribe(); }, [activeStep, methods.watch]); // 绑定按钮禁用状态 return ( <FormProvider {...methods}> {/* ...其他内容 */} <button disabled={!isStepValid} onClick={!isLastStep ? handleNext : onSubmit} > {isLastStep ? "submit" : "next"} </button> </FormProvider> ); }
方案2:使用React Hook Form的trigger方法验证指定字段
无需拆分Schema,直接通过trigger触发当前区块所有字段的验证:
function Welcome(): JSX.Element { // ...其他状态和逻辑 const [isStepValid, setIsStepValid] = useState<boolean>(false); const getCurrentSection = (step: number): string => { switch (step) { case 0: return "contactInfo"; case 1: return "eContacts"; case 2: return "pack"; case 3: return "vetInfo"; default: return ""; } }; const checkStepValidity = async () => { const currentStep = activeStep; if (currentStep === 3) { return setIsStepValid(methods.formState.isValid); } const currentSection = getCurrentSection(currentStep); // 生成当前区块下的完整字段名(如contactInfo.name) const sectionFields = Object.keys(methods.getValues(currentSection)).map( key => `${currentSection}.${key}` ); // 触发字段验证 const isValid = await methods.trigger(sectionFields); setIsStepValid(isValid); }; // 步骤切换和字段变化时触发验证(同方案1的useEffect) useEffect(() => { checkStepValidity(); }, [activeStep, methods.getValues]); useEffect(() => { const subscription = methods.watch((_, { name }) => { const currentSection = getCurrentSection(activeStep); if (name.startsWith(currentSection)) { checkStepValidity(); } }); return () => subscription.unsubscribe(); }, [activeStep, methods.watch]); // 绑定按钮禁用状态 return ( <FormProvider {...methods}> {/* ...其他内容 */} <button disabled={!isStepValid} onClick={!isLastStep ? handleNext : onSubmit} > {isLastStep ? "submit" : "next"} </button> </FormProvider> ); }
方案3:遍历区块字段检查错误状态
通过遍历当前区块的所有字段,结合formState.errors判断是否存在错误:
const handleDisabled = (): boolean => { const currentSection = getCurrentSection(activeStep); if (activeStep === 3) { return !methods.formState.isValid; } const sectionValues = methods.getValues(currentSection); // 遍历区块字段,检查是否有错误 for (const key of Object.keys(sectionValues)) { const fieldName = `${currentSection}.${key}`; if (methods.formState.errors[fieldName]) { return true; // 存在错误,按钮禁用 } } return false; // 无错误,按钮可用 };
注意:此方案依赖表单mode设置(如mode: "all"),确保字段错误能实时更新到formState.errors中。
内容的提问来源于stack exchange,提问作者eleventhaus
相关产品推荐
相关产品推荐

