reactjsmaterial-uitextfieldformikformik-material-ui

MUI textfield error and helpertext throwing undefined error despite formik being initialised


I'm not sure why I seem to be getting this issue - I've noticed it a couple of times when I try to use nested objects with formik, react and typescript. Formik doesn't appear to like data with nested objects it would seem?

enter image description here

I've doubled checked the initialisation of my form values and they are all as expected yet it seems the error= and helpertext= props on the textfield throw a cannot read properties of undefined (reading 'emailSubject') error whenever I try to load my form.

I've found that disabling them stops this error from occurring so it's clear these are where the issue is coming from but I can't seem to determine how to resolve the textfield seemingly thinking these nested properties are apparently undefined despite them being initialised.

I've put my component's code below to help possibly troubleshoot the issue.


const initialValues: ActionFormValues = {
    actionType: "send_email",
    recipient: "",
    email: {
        recipients: [],
        emailSubject: "",
        emailContent: ""
    }
};

const EmailForm = () => {
    const { t } = useTranslation();

    const [action, setAction] = useState<Action>(initialValues);
    const [isSubmissionValid, setIsSubmissionValid] = useState(false);

    const validationSchema = yup.object({
        recipient: yup
            .string()
            .email(t("FormValidation.Valid_Email"))
            .test("unique-recipient", t("FormValidation.Email_Exists"), function (value) {
                const { recipients } = this.parent.email; // Access recipients from the parent object
                return !recipients.includes(value);
            }),

        email: yup.object({
            emailSubject: yup.string().required("An email subject is required")
        })
    });

    const handleSubmit = (values: ActionFormValues) => {
        const submission = handleSubmissionCreation(values);
        console.log(submission);
    };

    const formik = useFormik<ActionFormValues>({
        initialValues: action,
        enableReinitialize: true,
        validationSchema,
        onSubmit: handleSubmit
    });

    useEffect(() => {
        let currentValues = formik.values;
        if (currentValues.actionType === "send_email") {
            if (currentValues.email.emailContent !== "" && currentValues.email.emailSubject !== "") {
                setIsSubmissionValid(true);
            }
        }
    }, [formik.values]);

    const handleSubmissionCreation = (submittedValues: ActionFormValues): Action => {
        if (submittedValues.actionType === "send_email") {
            return {
                actionType: submittedValues.actionType,
                email: {
                    recipients: action.email.recipients,
                    emailSubject: submittedValues.email.emailSubject,
                    emailContent: submittedValues.email.emailContent
                }
            };
        }
    };

    // Handler for adding email
    const handleAddEmail = (newEmail: string) => {
        if (newEmail) {
            // Update the email.recipients array in the state
            setAction((prevState) => ({
                ...prevState,
                email: {
                    ...prevState.email,
                    recipients: [...prevState.email.recipients, newEmail]
                }
            }));

            // Clear the recipient field and reset its state
            formik.setFieldValue("recipient", ""); // Clear the value
            formik.setFieldTouched("recipient", false); // Mark it as not touched
            formik.setFieldError("recipient", ""); // Clear any errors
        }
    };

    // Handler for deleting an recipient
    const handleDelete = (emailToDelete: string) => () => {
        setAction((prevState) => ({
            ...prevState,
            email: {
                ...prevState.email,
                recipients: prevState.email.recipients.filter((recipient) => recipient !== emailToDelete)
            }
        }));
    };

    return (
        <Box component="form" onSubmit={formik.handleSubmit}>
            <TextField
                fullWidth
                id="recipient"
                name="recipient"
                label={t("Common.Recipients")}
                value={formik.values.recipient}
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                error={formik.touched.recipient && Boolean(formik.errors.recipient)}
                helperText={formik.touched.recipient && formik.errors.recipient}
                onKeyDown={(ev) => {
                    if (ev.key === "Enter") {
                        if (!formik.isValid) {
                            ev.preventDefault();
                            return;
                        }
                        handleAddEmail(formik.values.recipient);
                        ev.preventDefault();
                    }
                }}
                inputProps={{ "data-testid": "recipient-field" }}
                InputProps={{
                    endAdornment: (
                        <InputAdornment position="end">
                            <IconButton
                                aria-label="add recipient"
                                onClick={() => handleAddEmail(formik.values.recipient)}
                                onMouseDown={(e) => e.preventDefault()}
                                disabled={!formik.isValid}
                            >
                                <AddCircle />
                            </IconButton>
                        </InputAdornment>
                    )
                }}
            />

            <Box className="recipient-container">
                {action &&
                    action.email.recipients.map((data, index) => (
                        <Chip key={index} label={data} onDelete={handleDelete(data)} />
                    ))}
            </Box>
            <TextField
                fullWidth
                id="emailSubject"
                name="email.emailSubject"
                label={t("Common.Email_Subject")}
                value={formik.values.email.emailSubject}
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                error={formik.touched.email.emailSubject && Boolean(formik.errors.email.emailSubject)}
                helperText={formik.touched.email.emailSubject && formik.errors.email.emailSubject}
            />

            <TextField
                fullWidth
                id="emailContent"
                name="email.emailContent"
                label={t("Common.Email_Message")}
                value={formik.values.email.emailContent}
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                multiline
                minRows={5}
            />

            <Button variant="contained" type="submit" fullWidth disabled={!isSubmissionValid}>
                Send Email
            </Button>
        </Box>
    );
};

export default EmailForm;

I've also tried the suggested steps here and updated the validation schema this also doesn't resolve the issue.

        recipient: yup
            .string()
            .email(t("FormValidation.Valid_Email"))
            .test("unique-recipient", t("FormValidation.Email_Exists"), function (value) {
                const { recipients } = this.parent.email; // Access recipients from the parent object
                return !recipients.includes(value);
            }),

        email: yup.object().shape({
            emailSubject: yup
                .string()
                .required("An email subject is required")
                .transform((value, originalValue) => (originalValue.trim() === "" ? null : value))
                .nullable()
        })
    });


Solution

  • I figured out the solution - this article was really helpful, apparently when dealing with nested validation with formik you have to utilise formik's getIn method in order to set helper and error text for nested data items.

    const validationSchema = yup.object({
            recipient: yup
                .string()
                .email(t("FormValidation.Valid_Email"))
                .test("unique-recipient", t("FormValidation.Email_Exists"), function (value) {
                    const { recipients } = this.parent.email; // Access recipients from the parent object
                    return !recipients.includes(value);
                }),
    
            email: yup.object().shape({
                emailSubject: yup.string().required("An email subject is required")
            })
        });
    
    <TextField
    fullWidth
    id="emailSubject"
    name="email.emailSubject"
    label={t("Common.Email_Subject")}
    value={formik.values.email.emailSubject}
    onChange={formik.handleChange}
    onBlur={formik.handleBlur}
    helperText={
       getIn(formik.touched, "email.emailSubject") &&
       getIn(formik.errors, "email.emailSubject")
    }
    error={Boolean(
       getIn(formik.touched, "email.emailSubject") &&
       getIn(formik.errors, "email.emailSubject")
    )}
    />