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

Table of Contents

Contributors

Picture of Md. Arfizur Rahman

Md. Arfizur Rahman

Tech Stack
0 +
Want to accelerate your software development your company?

It has become a prerequisite for companies to develop custom software products to stay competitive. Vivasoft's technical expertise.

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. So I decided to write a blog for anyone having the same issues. I’ve used Material-UI TextField 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 (

 

  );
};

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:

const initialValues = {
    name: '',
    age: '',
    email: '',
    phone: '',
    social: {
      facebook: '',
      linkedin: ''
    },
    password: '',
    confirmPassword: ''
};

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

  1. Formik: https://jaredpalmer.com/formik/docs/overview
  2. Material-UI: https://material-ui.com/
  3. Yup: https://github.com/jquense/yup
Tech Stack
0 +
Accelerate Your Software Development Potential with Us
With our innovative solutions and dedicated expertise, success is a guaranteed outcome. Let's accelerate together towards your goals and beyond.
Blogs You May Love

Don’t let understaffing hold you back. Maximize your team’s performance and reach your business goals with the best IT Staff Augmentation