How do I handle validations per-field in a Formik FieldArray?

Viewed 44

Scenario:

If there are 3 participants, all 3 participants will have the same set of questions but each of those questions have a list of "applied tickets" which will decide whether or not the question gets shown to a specific participant.

TLDR;

I need to know

  1. if it is possible to turn off validations for specific fields at runtime using yup (as far as I know this should not be possible since all field validations are provided at the start)
  2. what other alternatives do I have to get this validation done ? I'm okay with using Field level validations but I still want to be able to utilize yup because of all the in-built validation checks

Details:

I have a form which requires me to conditionally render specific fields in Formik but also remove the validation from it if the field doesn't exist.

The specific issue with this form, is that it contains multiple "sub-forms" handled via Formiks FieldArray. The structure of which looks like this.

Form Structure

The fields inside these sub-forms can be turned on and off but that doesn't apply for the validation schema.

So far, I've tried using the yup.when() function to conditionally render it and it does what's intended but it removes it for all of the elements in the form. The snippet is attached here,

/**
* Given a list of applied tickets, turn off the validation whenever the "id" property
* does not exist in the appliedTickets array
*/
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,
    });
}

Update 1 : Added Schema's

Main 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]);

Custom Question Schema

Each custom question here has a shape containing isPredefinedType. Based on this, there are two functions that dynamically generate the rest of the schema based on data provided by the custom question.

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 Answers

Ultimately I decided on switching to Field Level Validation whilst still using yup to create a custom and dynamic schema.

I called the Schema.validate function manually and assigned the result to the validate prop of the Formik Field component like below,

const validateField = (name: string, question: CustomQuestion, value: any) => {
    const validationResult = validate(question, value);
    if (validationResult.isError) setFieldError(name, validationResult.message);
        return validationResult.message;
    };
}
<FormikDropdown
    {...defaultProps}
    values={question.values}
    onValidate={(value) => validateField(questionFormFieldName, question, value)}
/>

To remove the field from the Form completely I had to call the setValue, setError and setTouched in the cleanup function of my useEffect hook in the FormikDropdown component that I created like so,

useEffect(() => {
    return () => {
        setValue(undefined);
        setError(undefined);
        setTouched(false);
    };
}, []);

This ultimately allowed me to omit specific fields from both being validated and also remove them if they were removed in the middle of the process so that any remaining data wouldn't exist.

I haven't fully tested out any edge cases however and it might still be buggy but as far as my initial testing goes, the approach works as intended

One small thing to note though. I have omitted the 3 functions from my dependency array. Reason being that for some reason, including them seems to cause an infinite re-rendering loop. It's being discussed on Formik's discussion thread in Github which still doesn't seem to have an official fix at the time of writing.

Related