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

如何在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,单独验证当前步骤对应的区块:

  1. 拆分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;
  1. 修改组件验证逻辑
    导入子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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 00:49:55