React: Form Validation (Nested schema) with Formik, Yup, and Material-UI
Mohammad Arfizur Rahman
Posted on September 23, 2021
Nowadays forms are crucial in any application. Building forms are pretty simple but adding validation to them may become a bit of a challenge. It gets even trickier when our forms have a complex structure, like binding the form fields to nested object properties and validating them. You’ll see shortly what I meant by that. The official React documentation doesn’t give you that much on how to handle forms. Well, I think this is ok since forms may have so many underlying business logic when it comes to validation. And React is only concerned about the UI not about the heavy business logic.
So, the background of this post is that a few days ago I was trying to validate a form and I struggled with it a bit as it contains nested schema. I used Material-UI for building the form and used Formik and Yup for validating it. Now I’ll discuss the terms I’ve mentioned (Formik, Yup, Material-UI).
Formik is a small library that helps us with managing states, handling validations and error messages, and handling form submission, etc. You can learn more on https://formik.org/.
Yup is a schema builder that helps us to create a clean validation object, which then can be provided to the validationSchema property of Formik. You can learn more on https://github.com/jquense/yup.
Material-UI provides well-designed input fields and form structure. You can learn about all the form elements and much more on https://material-ui.com/.
Install the required packages:
Let’s get started by installing the required packages using the following command:
npm install @material-ui formik yup
Building the form
We’ll build the form based on the following object:
const initialValues = {
name: '',
age: '',
email: '',
phone: '',
social: {
facebook: '',
linkedin: ''
},
password: '',
confirmPassword: ''
};
As you can see in this object initialValues there’s a nested object social with two properties, this is the nested object that I’ve mentioned earlier. Now let’s create the form.
We’ll import some Material-UI components which are optional and we’re only using these for a well-designed UI.
import clsx from ‘clsx’;
import PropTypes from ‘prop-types’;
import { makeStyles } from ‘@material-ui/styles’;
import { Card, CardHeader, CardContent, CardActions, Divider, Grid, TextField, Button } from ‘@material-ui/core’;
Here’s the full code of the form:
import React from 'react';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/styles';
import {
Card,
CardHeader,
CardContent,
CardActions,
Divider,
Grid,
TextField,
Button
} from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
padding: 0,
height: '100%'
},
actions: {
justifyContent: 'flex-end',
padding: theme.spacing(2)
}
}));
const SignUpForm = () => {
const classes = useStyles();
const initialValues = {
name: '',
age: '',
email: '',
phone: '',
social: {
facebook: '',
linkedin: ''
},
password: '',
confirmPassword: ''
};
return (
<Card className={clsx(classes.root)}>
<CardHeader title="Sign Up" />
<Divider />
<form autoComplete="off">
<CardContent>
<Grid container spacing={2}>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Name"
name="name"
type="text"
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Age"
name="age"
type="number"
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Email"
name="email"
type="text"
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Phone"
name="phone"
type="text"
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Facebook"
name="social.facebook"
type="text"
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="LinkedIn"
name="social.linkedin"
type="text"
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Password"
name="password"
type="password"
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Confirm Password"
name="confirmPassword"
type="password"
variant="outlined"
size="small"
/>
</Grid>
</Grid>
</CardContent>
<Divider />
<CardActions className={classes.actions}>
<Button color="primary" type="submit" variant="contained">
Save
</Button>
</CardActions>
</form>
</Card>
);
};
SignUpForm.propTypes = {
className: PropTypes.string
};
export default SignUpForm;
The form looks like the following:
Adding validations to the form
Our goal is to prevent users from submitting an invalid form. We’ll use different validation criteria as you’ll see shortly in the Yup validation schema. We’ll make the button disabled and enable it once all the validation criteria are met.
Let’s import the libraries required for the validation.
import * as Yup from ‘yup’;
import { Formik, getIn } from ‘formik’;
Let’s have a look at the yup validation schema object:
validationSchema={Yup.object().shape({
name: Yup.string().required('Name is required'),
age: Yup.number()
.required('Age is required')
.min(13, 'You must be at least 13 years old'),
email: Yup.string()
.email('Please enter a valid email')
.required('Email is required'),
phone: Yup.string().required('Phone is required'),
social: Yup.object().shape({
facebook: Yup.string().required('Facebook username is required'),
linkedin: Yup.string().required('LinkedIn username is required')
}),
password: Yup.string()
.required('Please enter your password')
.matches(
/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/,
'Password must contain 8 characters, one uppercase, one lowercase, one number and one special case Character'
),
confirmPassword: Yup.string()
.required('Please enter the password again')
.oneOf([Yup.ref('password'), null], "Passwords didn't match")
})}
You’ll notice that the nested object social holds another Yup schema.
social: Yup.object().shape({
facebook: Yup.string().required('Facebook username is required'),
linkedin: Yup.string().required('LinkedIn username is required')
}),
Now, let’s bring everything together, then we’ll discuss it.
import React from 'react';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/styles';
import * as Yup from 'yup';
import { Formik, getIn } from 'formik';
import {
Card,
CardHeader,
CardContent,
CardActions,
Divider,
Grid,
TextField,
Button
} from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
padding: 0,
height: '100%'
},
actions: {
justifyContent: 'flex-end',
padding: theme.spacing(2)
}
}));
const SignUpForm = () => {
const classes = useStyles();
const initialValues = {
name: '',
age: '',
email: '',
phone: '',
social: {
facebook: '',
linkedin: ''
},
password: '',
confirmPassword: ''
};
return (
<Card className={clsx(classes.root)}>
<CardHeader title="Sign Up" />
<Divider />
<Formik
initialValues={{
...initialValues
}}
validationSchema={Yup.object().shape({
name: Yup.string().required('Name is required'),
age: Yup.number()
.required('Age is required')
.min(13, 'You must be at least 13 years old'),
email: Yup.string()
.email('Please enter a valid email')
.required('Email is required'),
phone: Yup.string().required('Phone is required'),
social: Yup.object().shape({
facebook: Yup.string().required('Facebook username is required'),
linkedin: Yup.string().required('LinkedIn username is required')
}),
password: Yup.string()
.required('Please enter your password')
.matches(
/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/,
'Password must contain 8 characters, one uppercase, one lowercase, one number and one special case Character'
),
confirmPassword: Yup.string()
.required('Please enter the password again')
.oneOf([Yup.ref('password'), null], "Passwords didn't match")
})}
onSubmit={(values) => {
console.log(values);
}}>
{({
errors,
handleBlur,
handleChange,
handleSubmit,
isSubmitting,
isValid,
dirty,
touched,
values
}) => (
<form autoComplete="off" noValidate onSubmit={handleSubmit}>
<CardContent>
<Grid container spacing={2}>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(touched.name && errors.name)}
fullWidth
required
helperText={touched.name && errors.name}
label="Name"
name="name"
onBlur={handleBlur}
onChange={handleChange}
type="text"
value={values.name}
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(touched.age && errors.age)}
fullWidth
required
helperText={touched.age && errors.age}
label="Age"
name="age"
onBlur={handleBlur}
onChange={handleChange}
type="number"
value={values.age}
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(touched.email && errors.email)}
fullWidth
required
helperText={touched.email && errors.email}
label="Email"
name="email"
onBlur={handleBlur}
onChange={handleChange}
type="text"
value={values.email}
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(touched.phone && errors.phone)}
fullWidth
required
helperText={touched.phone && errors.phone}
label="Phone"
name="phone"
onBlur={handleBlur}
onChange={handleChange}
type="text"
value={values.phone}
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(
getIn(touched, 'social.facebook') &&
getIn(errors, 'social.facebook')
)}
fullWidth
required
helperText={
getIn(touched, 'social.facebook') &&
getIn(errors, 'social.facebook')
}
label="Facebook"
name="social.facebook"
onBlur={handleBlur}
onChange={handleChange}
type="text"
value={values.social.facebook}
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(
getIn(touched, 'social.linkedin') &&
getIn(errors, 'social.linkedin')
)}
fullWidth
required
helperText={
getIn(touched, 'social.linkedin') &&
getIn(errors, 'social.linkedin')
}
label="LinkedIn"
name="social.linkedin"
onBlur={handleBlur}
onChange={handleChange}
type="text"
value={values.social.linkedin}
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(touched.password && errors.password)}
fullWidth
required
helperText={touched.password && errors.password}
label="Password"
name="password"
onBlur={handleBlur}
onChange={handleChange}
type="password"
value={values.password}
variant="outlined"
size="small"
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
error={Boolean(
touched.confirmPassword && errors.confirmPassword
)}
fullWidth
required
helperText={
touched.confirmPassword && errors.confirmPassword
}
label="Confirm Password"
name="confirmPassword"
onBlur={handleBlur}
onChange={handleChange}
type="password"
value={values.confirmPassword}
variant="outlined"
size="small"
/>
</Grid>
</Grid>
</CardContent>
<Divider />
<CardActions className={classes.actions}>
<Button
color="primary"
disabled={Boolean(!isValid)}
type="submit"
variant="contained">
Save
</Button>
</CardActions>
</form>
)}
</Formik>
</Card>
);
};
SignUpForm.propTypes = {
className: PropTypes.string
};
export default SignUpForm;
We’ve added noValidated to the form to prevent HTML5 default form validation. Now let’s discuss the following text field:
<TextField
error={Boolean(touched.name && errors.name)}
fullWidth
required
helperText={touched.name && errors.name}
label="Name"
name="name"
onBlur={handleBlur}
onChange={handleChange}
type="text"
value={values.name}
variant="outlined"
size="small"
/>
Here error and helperText will be set conditionally if there’s an error and the input field is touched. Now let’s discuss the following text field with a slightly different syntax:
<TextField
error={Boolean(
getIn(touched, 'social.facebook') &&
getIn(errors, 'social.facebook')
)}
fullWidth
required
helperText={
getIn(touched, 'social.facebook') &&
getIn(errors, 'social.facebook')
}
label="Facebook"
name="social.facebook"
onBlur={handleBlur}
onChange={handleChange}
type="text"
value={values.social.facebook}
variant="outlined"
size="small"
/>
Here, because of the nested object, we’re setting error and helperText values differently. We’re using a helper function getIn() provided by Formik. Also, notice the value and name prop and how we set the value by accessing values.social.facebook, etc.
You’ll also notice we’ve conditionally made the button disabled for invalid form:
<Button
color="primary"
disabled={Boolean(!isValid)}
type="submit"
variant="contained">
Save
</Button>
After running the final code snippet if we try to submit the invalid form the output looks like this:
If you submit a valid form after filling out all the required fields, you’ll get the values automatically passed to the onSubmit() function by Formik. Then you can write the necessary code to send those data to the backend if you want.
onSubmit={(values) => {
console.log(values);
}}
Summary
Here, we described how can we validate forms and show error messages with Formik, Yup, and Material-UI. Most importantly we’ve worked with the nested object and discussed how to validate the nested object properties. We also discussed how to access the submitted data.
That’s it, I hope you’ve enjoyed this simple tutorial and this is helpful to you. To learn more about Formik, Yup and Material-UI please visit the following links in the Resources section.
Thanks!
Resources
- Formik: https://jaredpalmer.com/formik/docs/overview
- Material-UI: https://material-ui.com/
- Yup: https://github.com/jquense/yup
Posted on September 23, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.