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

如何在React Hook Form中结合Yup实现单字段分步验证

实现React Hook Form分步验证(仅验证当前步骤字段)

针对你的测验组件需求,这里提供两种实用的分步验证方案,只验证当前渲染组件对应的字段:

方案一:动态生成验证Schema

通过维护当前步骤状态,每次只加载对应字段的Yup规则,让React Hook Form仅验证当前步骤的字段。

代码示例

import { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

export const QUIZ_SCHEMA = yup.object().shape({
  radioGroup: yup.number().required('Please select an option to proceed'),
  arrangedText: yup.array().length(5, 'Please answer the question to proceed'),
  essay: yup
    .string()
    .required('Answer is required')
    .min(2, 'Min 2 characters required')
    .max(1000, 'Max 1000 characters allowed'),
});

const QuizComponent = () => {
  // 维护当前步骤,对应表单字段名
  const [currentStep, setCurrentStep] = useState('radioGroup');

  const {
    control,
    handleSubmit,
    formState: { errors, isValid },
    reset,
  } = useForm({
    defaultValues: {
      radioGroup: undefined,
      arrangedText: [],
      essay: '',
    },
    // 动态生成当前步骤的验证Schema
    resolver: yupResolver(
      yup.object().shape({
        [currentStep]: QUIZ_SCHEMA.fields[currentStep],
      })
    ),
  });

  const handleNext = () => {
    if (!isValid) return;
    
    // 切换步骤逻辑
    switch (currentStep) {
      case 'radioGroup':
        setCurrentStep('arrangedText');
        break;
      case 'arrangedText':
        setCurrentStep('essay');
        break;
      case 'essay':
        handleSubmit((data) => {
          console.log('最终提交数据:', data);
        })();
        break;
      default:
        break;
    }
  };

  // 渲染当前步骤组件
  const renderStepComponent = () => {
    switch (currentStep) {
      case 'radioGroup':
        return (
          <div>
            <label>选择一个选项:</label>
            <Controller
              name="radioGroup"
              control={control}
              render={({ field }) => (
                <>
                  <input type="radio" {...field} value={1} /> 选项1
                  <input type="radio" {...field} value={2} /> 选项2
                  <input type="radio" {...field} value={3} /> 选项3
                </>
              )}
            />
            {errors.radioGroup && <p style={{color: 'red'}}>{errors.radioGroup.message}</p>}
          </div>
        );
      case 'arrangedText':
        return (
          <div>
            <label>排列5项文本(逗号分隔数字):</label>
            <Controller
              name="arrangedText"
              control={control}
              render={({ field }) => (
                <input
                  type="text"
                  onChange={(e) => field.onChange(e.target.value.split(',').map(Number))}
                  placeholder="例如:1,2,3,4,5"
                />
              )}
            />
            {errors.arrangedText && <p style={{color: 'red'}}>{errors.arrangedText.message}</p>}
          </div>
        );
      case 'essay':
        return (
          <div>
            <label>输入回答:</label>
            <Controller
              name="essay"
              control={control}
              render={({ field }) => (
                <textarea {...field} rows={5} placeholder="至少输入2个字符" />
              )}
            />
            {errors.essay && <p style={{color: 'red'}}>{errors.essay.message}</p>}
          </div>
        );
      default:
        return null;
    }
  };

  return (
    <div>
      {renderStepComponent()}
      <button onClick={handleNext} disabled={!isValid}>
        {currentStep === 'essay' ? '提交' : '下一步'}
      </button>
    </div>
  );
};

export default QuizComponent;

方案二:手动触发单个字段验证

保留完整的Yup Schema,通过trigger方法仅验证当前步骤对应的字段,适合需要最终提交时统一验证所有字段的场景。

代码示例

import { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

export const QUIZ_SCHEMA = yup.object().shape({
  radioGroup: yup.number().required('Please select an option to proceed'),
  arrangedText: yup.array().length(5, 'Please answer the question to proceed'),
  essay: yup
    .string()
    .required('Answer is required')
    .min(2, 'Min 2 characters required')
    .max(1000, 'Max 1000 characters allowed'),
});

const QuizComponent = () => {
  const [currentStep, setCurrentStep] = useState('radioGroup');

  const {
    control,
    handleSubmit,
    trigger,
    formState: { errors },
    reset,
  } = useForm({
    defaultValues: {
      radioGroup: undefined,
      arrangedText: [],
      essay: '',
    },
    resolver: yupResolver(QUIZ_SCHEMA),
  });

  const handleNext = async () => {
    // 仅触发当前步骤字段的验证
    const isValid = await trigger(currentStep);
    if (!isValid) return;

    // 切换步骤或提交
    switch (currentStep) {
      case 'radioGroup':
        setCurrentStep('arrangedText');
        break;
      case 'arrangedText':
        setCurrentStep('essay');
        break;
      case 'essay':
        handleSubmit((data) => {
          console.log('最终提交数据:', data);
        })();
        break;
      default:
        break;
    }
  };

  // 渲染当前步骤组件(同方案一逻辑)
  const renderStepComponent = () => {
    switch (currentStep) {
      case 'radioGroup':
        return (
          <div>
            <label>选择一个选项:</label>
            <Controller
              name="radioGroup"
              control={control}
              render={({ field }) => (
                <>
                  <input type="radio" {...field} value={1} /> 选项1
                  <input type="radio" {...field} value={2} /> 选项2
                  <input type="radio" {...field} value={3} /> 选项3
                </>
              )}
            />
            {errors.radioGroup && <p style={{color: 'red'}}>{errors.radioGroup.message}</p>}
          </div>
        );
      case 'arrangedText':
        return (
          <div>
            <label>排列5项文本(逗号分隔数字):</label>
            <Controller
              name="arrangedText"
              control={control}
              render={({ field }) => (
                <input
                  type="text"
                  onChange={(e) => field.onChange(e.target.value.split(',').map(Number))}
                  placeholder="例如:1,2,3,4,5"
                />
              )}
            />
            {errors.arrangedText && <p style={{color: 'red'}}>{errors.arrangedText.message}</p>}
          </div>
        );
      case 'essay':
        return (
          <div>
            <label>输入回答:</label>
            <Controller
              name="essay"
              control={control}
              render={({ field }) => (
                <textarea {...field} rows={5} placeholder="至少输入2个字符" />
              )}
            />
            {errors.essay && <p style={{color: 'red'}}>{errors.essay.message}</p>}
          </div>
        );
      default:
        return null;
    }
  };

  return (
    <div>
      {renderStepComponent()}
      <button onClick={handleNext}>
        {currentStep === 'essay' ? '提交' : '下一步'}
      </button>
    </div>
  );
};

export default QuizComponent;

方案对比

  • 动态Schema方案:全局isValid会准确反映当前步骤的验证状态,适合用isValid禁用下一步按钮的场景,但每次切换步骤会重新初始化验证逻辑。
  • 手动trigger方案:保留完整Schema,最终提交时会自动验证所有字段,灵活度更高,但需要手动处理验证触发,不依赖全局isValid状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 02:23:17