DEV Community

Stephanie Opala
Stephanie Opala

Posted on

 

How to build a multi-step form using React.js + Material UI

Forms are used often in applications to capture information from users. A multi-step form is a form where its content is grouped into various steps with smaller pieces of the content. These types of forms are mostly used in cases where the content on the form is a lot, therefore, breaking it into smaller sections improves the user experience.

This article covers the steps on how to make a multi-step form using React.js, Material UI, and Formik and Yup for form validation.

Table of contents

Prerequisites

To follow along, you will need to have:

  • Basic knowledge of React JS.
  • Basic knowledge of Formik.

Getting Started

Create a new React project using the commands below in your terminal.

npx create-react-app multistep-form
Enter fullscreen mode Exit fullscreen mode
cd multistep-form
Enter fullscreen mode Exit fullscreen mode

Install Material UI for styling. This library also contains some components that we
will use to build the form.

npm install @mui/material @emotion/react @emotion/styled
Enter fullscreen mode Exit fullscreen mode

Next, install Formik and Yup for form handling and validation.

npm install formik yup
Enter fullscreen mode Exit fullscreen mode

Start the project on localhost.

npm start
Enter fullscreen mode Exit fullscreen mode

Creating the parent component

Our form will have three steps. The first step will contain the account details such as email and password.
The second step will contain a user's personal information such as name, phone number, and residence. The last step
is a review step where all the information that the user has entered in the form is displayed before he/she submits the form. We will have a parent component Form and three child components namely, AccountDetails, PersonalInfo
and ReviewInfo.

Navigate to the src folder and create a folder named components. Inside the components folder, create a file, Form.jsx.
This will be the parent component. Create three other files namely, AccountDetails.jsx, PersonalInfo.jsx
and ReviewInfo.jsx that will contain the child components. In Form.jsx, add the following code.

import { useState } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import {
  Box,
  Stepper,
  Step,
  StepLabel,
  Grid,
  FormHelperText,
  Button
} from '@mui/material';
import PersonalInfo from './PersonalInfo';
import AccountDetails from './AccountDetails';
import ReviewInfo from './ReviewInfo';

const steps = [' Account Details', 'Personal Info', 'Review and Submit'];

const Form = () => {
  const [activeStep, setActiveStep] = useState(0);

  const handleBack = () => {
    setActiveStep((prevStep) => prevStep - 1);
  };

  const formik = useFormik({
    initialValues: {
      email: '',
      password: '',
      confirmPassword: '',
      firstName: '',
      lastName: '',
      phone: '',
      residence: ''
    },
    validationSchema: Yup.object().shape({
      email: Yup.string()
        .required('Email is required')
        .email('Invalid email'),
      password: Yup.string()
        .min(8),
      confirmPassword: Yup.string()
        .min(8)
        .oneOf([Yup.ref('password')], 'Passwords do not match'),
      firstName: Yup.string()
        .required('First Name is required'),
      lastName: Yup.string()
        .required('Last Name is required'),
    }),
    onSubmit: () => {
      if (activeStep === steps.length - 1) {
        console.log('last step');
      } else {
        setActiveStep((prevStep) => prevStep + 1);
      }
    }
  });

  const formContent = (step) => {
    switch(step) {
      case 0:
        return <AccountDetails formik={formik} />;
      case 1:
        return <PersonalInfo formik={formik} />;
      case 2:
        return <ReviewInfo formik={formik} />;
      default:
        return <div>404: Not Found</div>
    }
  };

  return (
    <Box
      sx={{
        maxWidth: '600px',
        padding: 2
      }}
    >
      <Stepper
        activeStep={activeStep}
        orientation="horizontal"
      >
        {steps.map((label, index) => (
          <Step key={index}>
            <StepLabel>{label}</StepLabel>
          </Step>
        ))}
      </Stepper>
      <Grid container>
        <Grid
          item
          xs={12}
          sx={{ padding: '20px' }}
        >
          {formContent(activeStep)}
        </Grid>
        {formik.errors.submit && (
          <Grid
            item
            xs={12}
          >
            <FormHelperText error>
              {formik.errors.submit}
            </FormHelperText>
          </Grid>
        )}
        <Grid
          item
          xs={12}
        >
          <Button
            disabled={activeStep === 0}
            onClick={handleBack}
          >
            Back
          </Button>
          {activeStep === steps.length - 1 ? (
            <Button>
              Submit
            </Button>
          ) : (
            <Button onClick={formik.handleSubmit}>
              Next
            </Button>
          ) }
        </Grid>
      </Grid>
    </Box>
  )
}

export default Form;
Enter fullscreen mode Exit fullscreen mode

At the top of Form.jsx, we import the useState hook from react. We then import the useFormik hook and Yup for form handling and validation. Next, we import Material UI components and lastly, the child components.
After the imports, we declare a variable steps that is an array of strings with the names of our steps in the form.

The Form component returns JSX which includes the <Stepper></Stepper> material UI component. For this project, we will use a horizontal linear stepper. A linear stepper allows the user to complete the steps in sequence. This component accepts several props which include activeStep, orientation etc. The activeStep prop is a zero-based index. In our code, we initialize the state activeStep and set it to 0.

  const [activeStep, setActiveStep] = useState(0);
Enter fullscreen mode Exit fullscreen mode

We then pass it as props to the Stepper component.

<Stepper
  activeStep={activeStep}
  orientation="horizontal"
>
  {steps.map((label, index) => (
    <Step key={index}>
      <StepLabel>{label}</StepLabel>
    </Step>
  ))}
</Stepper>
Enter fullscreen mode Exit fullscreen mode

The orientation prop is the layout flow direction. The value can either be horizontal or vertical. Inside the <Stepper></Stepper> component we map through the steps array and return a <Step></Step> component for each item.

Below the <Stepper></Stepper>, we have a grid container with grid items. The first grid item wraps around a function call, formContent, that accepts the activeStep prop and returns a switch statement with the respective child component. The other grid item wraps around the back and next/submit buttons. The back button is disabled if the activeStep is equal to 0, which means that the user is on the first step of the form.
To display the next or submit button, we have a ternary operator to check if the user is on the last step. If so, we display the submit button, if not, we display the next button. The back and submit buttons have an onClick event handler that calls the handleBack and handleSubmit functions respectively.

Form.jsx also contains our validation schema and we pass the formik object to the child components as props.

Creating the child components

The three children components recieve the formik object that has the form values, errors, and onSubmit event listener among other properties.

AccountDetails.jsx

import {
  Grid,
  TextField,
  FormHelperText
} from "@mui/material";

const AccountDetails = (props) => {
  const { formik } = props;
  return (
    <Grid
      container
      spacing={2}
    >
      <Grid
        item
        xs={12}
      >
        <TextField
          name="email"
          label="Email"
          variant="outlined"
          type="email"
          fullWidth
          size="small"
          error={Boolean(formik.touched.email && formik.errors.email)}
          onChange={formik.handleChange}
          value={formik.values.email}
        />
      </Grid>
      <Grid
        item
        xs={12}
      >
        <TextField
          name="password"
          label="Password"
          variant="outlined"
          size='small'
          type="password"
          fullWidth
          error={Boolean(formik.touched.password && formik.errors.password)}
          onChange={formik.handleChange}
          value={formik.values.password}
        />
      </Grid>
      <Grid
        item
        xs={12}
      >
        <TextField
          name="confirmPassword"
          label="Confirm Password"
          variant="outlined"
          size="small"
          type="password"
          fullWidth
          error={Boolean(formik.touched.confirmPassword && formik.errors.confirmPassword)}
          onChange={formik.handleChange}
          value={formik.values.confirmPassword}
        />
      </Grid>
      {formik.errors.submit && (
        <Grid
          item
          xs={12}
        >
          <FormHelperText error>
            {formik.errors.submit}
          </FormHelperText>
        </Grid>
      )}
    </Grid>
  )
}

export default AccountDetails
Enter fullscreen mode Exit fullscreen mode

PersonalInfo.jsx

import {
  TextField,
  Grid
} from '@mui/material';

const PersonalInfo = (props) => {
  const { formik } = props;
  return (
    <Grid
      container
      spacing={2}
    >
      <Grid
        item
        xs={6}
      >
        <TextField
          name="firstName"
          label="First Name"
          variant="outlined"
          size='small'
          fullWidth
          value={formik.values.firstName}
          onChange={formik.handleChange}
          error={formik.touched.firstName && Boolean(formik.errors.firstName)}
          helperText={formik.touched.firstName && formik.errors.firstName}
        />
      </Grid>
      <Grid
        item
        xs={6}
      >
        <TextField
          name="lastName"
          label="Last Name"
          variant="outlined"
          size="small"
          fullWidth
          value={formik.values.lastName}
          onChange={formik.handleChange}
          error={formik.touched.lastName && Boolean(formik.errors.lastNamel)}
          helperText={formik.touched.lastName && formik.errors.lastName}
        />
      </Grid>
      <Grid
        item
        xs={12}
      >
        <TextField
          name="phone"
          label="Phone Number"
          variant="outlined"
          type="phone"
          fullWidth
          size="small"
          value={formik.values.phone}
          onChange={formik.handleChange}
          error={formik.touched.phone && Boolean(formik.errors.phone)}
          helperText={formik.touched.phone && formik.errors.phone}
        />
      </Grid>
      <Grid
        item
        xs={12}
      >
        <TextField
          name="residence"
          label="Residence"
          variant="outlined"
          size="small"
          fullWidth
          value={formik.values.residence}
          onChange={formik.handleChange}
          error={formik.touched.residence && Boolean(formik.errors.residence)}
          helperText={formik.touched.residence && formik.errors.residence}
        />
      </Grid>
    </Grid>
  )
}

export default PersonalInfo
Enter fullscreen mode Exit fullscreen mode

ReviewInfo.jsx

This file will display the form values that the user entered in the form.

import {
  Typography,
  List,
  ListItem,
  ListItemText
} from '@mui/material';

const ReviewInfo = ({ formik }) => {
  const { values } = formik;
  return (
    <>
      <Typography variant="overline" >
        Account Details
      </Typography>
      <List>
        <ListItem>
          <ListItemText
            primary="Email"
            secondary={values.email}
          />
        </ListItem>
      </List>
      <Typography variant="overline">
        Personal Information
      </Typography>
      <List>
        <ListItem>
          <ListItemText
            primary="First Name"
            secondary={values.firstName}
          />
        </ListItem>
        <ListItem>
          <ListItemText
            primary="Last Name"
            secondary={values.lastName}
          />
        </ListItem>
        <ListItem>
          <ListItemText
            primary="Phone Number"
            secondary={values.phone}
          />
        </ListItem>
        <ListItem>
          <ListItemText
            primary="Residence"
            secondary={values.residence}
          />
        </ListItem>
      </List>
    </>
  )
}

export default ReviewInfo
Enter fullscreen mode Exit fullscreen mode

Lastly, import the Form.jsx file in App.jsx.

import './App.css';
import Form from './components/Form';

const App = () => {
  return (
    <div className="App">
      <Form />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, we have covered how to create a multi-step form using React, Material UI, and Formik and Yup for form handling and validation. If you would like to customize your form further, you can check the Material UI documentation on how you can do that using the Stepper component.

Oldest comments (0)

Visualizing Promises and Async/Await 🤯

async await

☝️ Check out this all-time classic DEV post