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?
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()
})
});
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")
)}
/>