How to build a multi-step form using React.js + Material UI
Tags: react, webdev, javascript
Published: 2022-11-09 11:17:55

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](#prerequisites)
- [Getting Started](#getting-started)
- [Creating the parent component](#creating-the-form-component)
- [Creating the child components](#creating-the-child-components)
- [Conclusion](#conclusion)

### 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.
```bash
npx create-react-app multistep-form
```

```bash
cd multistep-form
```

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

```bash
npm install @mui/material @emotion/react @emotion/styled
```

Next, install Formik and Yup for form handling and validation.
```bash
npm install formik yup
```

Start the project on localhost.
```bash
npm start
```

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

```javascript
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;
```

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

```javascript
  const [activeStep, setActiveStep] = useState(0);
```
We then pass it as props to the `Stepper` component.

```javascript
<Stepper
  activeStep={activeStep}
  orientation="horizontal"
>
  {steps.map((label, index) => (
    <Step key={index}>
      <StepLabel>{label}</StepLabel>
    </Step>
  ))}
</Stepper>
```

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
```javascript
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
```

#### PersonalInfo.jsx
```javascript
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
```

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

```javascript
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
```

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

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

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

export default App;
```

### 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](https://mui.com/material-ui/react-stepper/) on how you can do that using the `Stepper` component.