DEV Community

Cover image for React: Form Validation (Nested schema) with Formik, Yup, and Material-UI
Mohammad Arfizur Rahman
Mohammad Arfizur Rahman

Posted on • Edited on

React: Form Validation (Nested schema) with Formik, Yup, and Material-UI

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: ''
};


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

The form looks like the following:
Signup form

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")
  })}


Enter fullscreen mode Exit fullscreen mode

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')
}),


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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"
/>


Enter fullscreen mode Exit fullscreen mode

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"
/>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

After running the final code snippet if we try to submit the invalid form the output looks like this:

Invalid form

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);
}}

Enter fullscreen mode Exit fullscreen mode




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
  2. Material-UI: https://material-ui.com/
  3. Yup: https://github.com/jquense/yup

Top comments (2)

Collapse
 
cuginoale profile image
Alessio Carnevale

Hey, interesting article here but I do believe that form handling should be way easier.
There's a lot of code there that needs to be digested, maintained and tested.

I discuss a simpler approach here:
dev.to/cuginoale/form-validationsu...

Your perspective on this would be highly appreciated :)

Collapse
 
strk14_ profile image
Aman Shukla

Super useful ✌️