React: Form Validation (Nested schema) with Formik, Yup, and Material-UI
Tags: react, showdev, webdev, javascript
Published: 2021-09-23 11:02:45

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](https://reactjs.org/docs/forms.html). 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](https://material-ui.com/) [<TextField/>](https://material-ui.com/components/text-fields) for building the form and used [Formik](https://formik.org/) and [Yup](https://github.com/jquense/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/](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](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/](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:
```js
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:
```js
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:
![Signup form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hx7bup9l8y5wkunfnt6e.jpeg)

## 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:

```js
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.
```js
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.

```js
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:

```js
<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:

```js
<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:
```js
<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:

![Invalid form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jug9v1q1u7qal3gc65ti.jpeg)

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.

```js
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
1. Formik: [https://jaredpalmer.com/formik/docs/overview](https://jaredpalmer.com/formik/docs/overview)
2. Material-UI: [https://material-ui.com/](https://material-ui.com/)
3. Yup: [https://github.com/jquense/yup](https://github.com/jquense/yup)