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

Formik FieldArray字段级验证:Yup运行时开关验证及替代方案

问题描述

场景

有3位参与者,所有参与者需填写相同的问题集,但每个问题都有「适用工单」列表,用于决定是否向特定参与者展示该问题。

核心需求

  • 能否通过yup在运行时关闭特定字段的验证?(目前了解到所有字段验证初始已定义,似乎不可行)
  • 还有哪些验证实现方案?可接受字段级验证,但希望保留yup的内置校验能力

详细说明

使用Formik表单,需要条件渲染特定字段,要求字段不显示时移除其验证规则。表单包含多个通过Formik FieldArray处理的「子表单」,子表单内的字段可动态显示/隐藏,但验证规则不会随之变化。

目前尝试用yup.when()实现条件验证,虽能达到效果,但会移除表单中所有元素的该字段验证,代码如下:

/**
* 给定适用工单列表,当"id"属性不在appliedTickets数组中时,关闭验证
*/
if (field.appliedTickets.length > 0) {
    schema = schema.when("id", {
        is: (value: any) => field.appliedTickets.every((t) => t !== value),
        then: (s) => s.notRequired(),
        otherwise: (s) => s,
    });
}

更新1:补充Schema

主Schema

const personalDetailsSchema = useMemo(() => {
        const ticketSchema = {
            id: string().strip(),
            firstName: string()
                .trim()
                .required(getErrorMessage("firstName.label", { required: true })),
            lastName: string()
                .trim()
                .required(getErrorMessage("lastName.label", { required: true })),
            gender: mixed<Gender>()
                .required(getErrorMessage("gender.label", { required: true }))
                .defined()
                .default(Gender.male),
            email: string()
                .email("Must be a valid email")
                .required(getErrorMessage("email.label", { required: true })),
            dateOfBirth: date()
                .required(getErrorMessage("dateOfBirth.label", { required: true }))
                .typeError("A valid date is required")
                .nullable()
                .default(null),
            nationality: string()
                .oneOf<Country["abbr"]>(
                    allCountries.map((c) => c.abbr),
                    "An available nationality is required"
                )
                .required(getErrorMessage("nationality.label", { required: true }))
                .defined(),
            extraInfo: customQuestionSchema
                ? object({ id: string().strip(), ...customQuestionSchema })
                : mixed().notRequired(),
        };

        const personalDetailsSchema = object({
            tickets: array(object(ticketSchema)),
        });

        return personalDetailsSchema;
    }, [getErrorMessage, customQuestionSchema]);

自定义问题Schema

每个自定义问题包含isPredefinedType属性,基于该属性,通过两个函数动态生成剩余Schema:

const customQuestionSchema: Nullable<Record<string, AnySchema>> =
        useMemo(() => {
            if (!customQuestions) {
                return;
            }

            const jointSchema = customQuestions?.reduce((previous, current) => {
                if (current.isPreDefinedType) {
                    const presetQuestionSchema = getPredefinedSchemaFor(current);
                    if (presetQuestionSchema) {
                        return Object.assign(previous, {
                            [presetQuestionSchema.name]: presetQuestionSchema.objectSchema,
                        });
                    }
                }

                const { name, objectSchema } = getCustomSchemaFor(current);
                return Object.assign(previous, {
                    [name]: objectSchema,
                });
            }, {});

            return jointSchema;
        }, [customQuestions, getCustomSchemaFor, getPredefinedSchemaFor]);

const personalDetailsSchema = object({
    tickets: array(object(ticketSchema)),
})

解决方案

1. 关于通过Yup在运行时关闭特定字段验证

Yup本身支持基于字段值的动态验证,你之前的写法问题在于:schema.when("id")是对整个Schema生效,而非针对数组中的单个子表单元素。要实现只对特定参与者(数组中特定ticket项)的字段关闭验证,需要在数组项的Schema内部做条件判断:

方式一:字段级when关联当前ticket的id

在定义ticketSchema时,针对需要条件验证的字段,直接在字段内部使用when,关联当前ticket的id和该字段的适用工单列表:

// 示例:为自定义字段添加条件验证
customField: string()
  .when("id", {
    is: (ticketId) => field.appliedTickets.includes(ticketId),
    then: (schema) => schema.required("该字段必填"),
    otherwise: (schema) => schema.notRequired(),
  })

这样就能确保只有当当前ticket的id在适用列表中时,才会启用该字段的验证,不会影响其他ticket项。

方式二:自定义test验证逻辑

如果需要更灵活的判断,可以用Yup的test方法,直接获取当前表单上下文:

customField: string()
  .test(
    "required-if-applicable",
    "该字段必填",
    function(value) {
      const { id } = this.parent; // 获取当前ticket的id
      if (!field.appliedTickets.includes(id)) {
        return true; // 不适用时直接通过验证
      }
      return !!value; // 适用时验证是否有值
    }
  )

2. 其他验证实现方案

方案一:动态生成Schema

既然每个问题的适用工单是已知的,可以在生成customQuestionSchema时,直接根据适用工单列表动态决定是否添加验证规则:

const jointSchema = customQuestions?.reduce((previous, current) => {
  let fieldSchema;
  if (current.isPreDefinedType) {
    fieldSchema = getPredefinedSchemaFor(current).objectSchema;
  } else {
    fieldSchema = getCustomSchemaFor(current).objectSchema;
  }

  // 根据适用工单列表修改验证规则
  if (current.appliedTickets.length > 0) {
    fieldSchema = fieldSchema.when("id", {
      is: (ticketId) => !current.appliedTickets.includes(ticketId),
      then: (s) => s.notRequired(),
      otherwise: (s) => s,
    });
  }

  return { ...previous, [current.name]: fieldSchema };
}, {});

这样生成的Schema本身就包含了条件验证逻辑,无需运行时修改。

方案二:结合Formik字段级验证

Formik允许在validate函数中做字段级验证,同时调用Yup的验证方法复用内置规则:

const validate = (values) => {
  const errors = {};
  // 遍历每个ticket
  values.tickets.forEach((ticket, index) => {
    errors.tickets = errors.tickets || [];
    // 遍历每个自定义问题
    Object.keys(customQuestionSchema || {}).forEach((fieldName) => {
      const field = customQuestions.find(q => q.name === fieldName);
      // 如果当前ticket不在适用列表中,跳过验证
      if (!field.appliedTickets.includes(ticket.id)) return;
      // 用Yup验证该字段
      try {
        customQuestionSchema[fieldName].validateSync(ticket.extraInfo[fieldName]);
      } catch (err) {
        errors.tickets[index] = errors.tickets[index] || {};
        errors.tickets[index].extraInfo = errors.tickets[index].extraInfo || {};
        errors.tickets[index].extraInfo[fieldName] = err.message;
      }
    });
  });
  return errors;
};

// 在Formik组件中使用
<Formik
  initialValues={...}
  validate={validate}
  onSubmit={...}
>
  {/* 表单内容 */}
</Formik>

这种方式可以完全控制哪些字段需要验证,同时保留Yup的内置校验能力。

方案三:手动控制Formik验证状态

在字段显示/隐藏时,调用Formik的setFieldError清除或添加错误:

// 字段隐藏时清除错误
const handleFieldHide = (ticketIndex, fieldName) => {
  props.setFieldError(`tickets[${ticketIndex}].extraInfo.${fieldName}`, undefined);
};

适合需要手动干预验证状态的场景,配合条件渲染使用。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 00:45:53