• Skip to primary navigation
  • Skip to main content
  • Skip to footer

Codemotion Magazine

We code the future. Together

  • Discover
    • Events
    • Community
    • Partners
    • Become a partner
    • Hackathons
  • Magazine
    • Backend
    • Frontend
    • AI/ML
    • DevOps
    • Dev Life
    • Soft Skills
    • Infographics
  • Talent
    • Discover Talent
    • Jobs
    • Manifesto
  • Companies
  • For Business
    • EN
    • IT
    • ES
  • Sign in
ads

Noa ShtangAugust 8, 2024 8 min read

Building reusable multiple-step form in ReactJS

Frontend
React library: all you need to know about it.
facebooktwitterlinkedinreddit

Introduction

Building user-friendly multiple-step forms in ReactJS can be challenging. Developers often struggle with managing form states, ensuring seamless user navigation, handling validation, and maintaining a smooth user experience. Adhering to best practices is necessary for these forms to become cumbersome, leading to user frustration and increased abandonment rates.

Recommended article
May 26, 2025

10 React expert tips that will change your life!

Lucilla Tomassi

Lucilla Tomassi

Frontend

In this article, I provide a comprehensive guide to best practices for building multiple-step forms in ReactJS. I outline practical strategies for state management, user flow control, and validation techniques. By adhering to these guidelines, you can create intuitive and efficient multi-step forms.

Prerequisites

To follow along with this guide, you should have a basic understanding of:

  • ReactJS and its core concepts (components, props, state)
  • JavaScript ES6 syntax

Directory structure

We will create a directory structure that looks like this:

src/
|-- components/
| |-- Button/
| |-- Icon/
|-- containers/
| |-- StepsController/
| | |-- StepsController.js
| | |-- StepsIndicator.js
| | |-- StepsController.module.scss
| |-- Form/
| | |-- FirstStep.js
| | |-- SecondStep.js
| | |-- ThirdStep.js
| | |-- Form.js
| | |-- Form.module.scss
|-- icons/
|--checked.svg
|-- services/
| |--manageValidation.js
|-- App.js

Creating the Form component

The Form component will control the form state and pass each step component to the StepsController. Each step of the form will be a separate component. The Form component handles validation logic, field changes, and submission.

Form.js 

import React, { useState } from "react";
import StepsController from "../StepsController/StepsController";
import FirstStep from "./FirstStep";
import SecondStep from './SecondStep'
import ThirdStep from './ThirdStep'
import { checkIsValidFormSteps, updateErrorsFormSteps } from "../../services/manageValidation";
import styles from "./Form.module.scss";


const SpotForm = () => {
   const [validationError, setValidationError] = useState([]);
   const [formData, setFormData] = useState({
       name: '',
       website: '',
       address: '',
       email: '',
       description: '',
       foundationDate: ''
   });
   const handleFieldChange = (name, value, isRequired) => {
       setFormData({ ...formData, [name]: value })
       isRequired && setValidationError(
           validationError.filter((errorItem) => errorItem !== name)
       )
   }
   const manageNextStepValidation = (step) => {
       const isValid = checkIsValidFormSteps({ formData, step });
       if (!isValid) {
           updateErrorsFormSteps({
               formData,
               step,
               setValidationError
           });
           return false
       }
       if (step === 3 && isValid) {
           handleSubmit()
       }
       return true
   }
   const handleSubmit = () => {
       alert("The form is valid. You can now submit the data: to the server.")
   };
   const steps = [
       <FirstStep formData={formData} validationError={validationError} handleFieldChange={handleFieldChange} />,
       <SecondStep formData={formData} validationError={validationError} handleFieldChange={handleFieldChange} />,
       <ThirdStep formData={formData} validationError={validationError} handleFieldChange={handleFieldChange} />
   ]
   return (
       <div className={styles.container}>
           <StepsController
               formTitle="Add a new company"
               manageNextStepValidation={manageNextStepValidation}
               steps={steps}
               stepsAmount={3}
           />
       </div>
   )
};
export default SpotForm;Code language: JavaScript (javascript)

Creating step components

Step components are stateless; they receive formData, handleFieldChange, and validationError as props from the Form component. In this example, I use the Input component from “reactstrap”.

FirstStep.js 

import React from 'react'
import cx from "classnames";
import { Input } from 'reactstrap';
import styles from './Form.module.scss'


const FirstStep = ({ validationError, formData, handleFieldChange }) => {


   return (
       <div className={styles.container}>
           <h2 className={styles.title}>General</h2>
           <div className={styles.formItem}>
               <label className={styles.fieldLabel} htmlFor="name"><span className={styles.asterisk}>*</span> company name</label>
               <Input
                   id="name"
                   name="name"
                   placeholder="Company name"
                   value={formData.name}
                   onChange={(event) => {
                       handleFieldChange("name", event.target.value, true)
                   }}
                   className={cx(styles.input, {
                       [styles.inputError]: validationError.includes("name"),
                   })}
               />
           </div>
           <div className={styles.textareaContainer}>
               <label className={styles.fieldLabel} htmlFor="text">description</label>
               <Input
                   id="text"
                   name="text"
                   className={cx(styles.textarea, {
                       [styles.inputError]: validationError.includes("description"),
                   })}
                   type="textarea"
                   placeholder="Description"
                   onChange={(event) => {
                       handleFieldChange("description", event.target.value)
                   }}
                   value={formData.description}
               />
           </div>
       </div >
   )
}


export default FirstStep
Code language: JavaScript (javascript)

SecondStep.js

import React from 'react'
import cx from "classnames";
import { Input } from 'reactstrap';
import styles from './Form.module.scss'
const SecondStep = ({ validationError, formData, handleFieldChange }) => {
   return (
       <div className={styles.container}>
           <h2 className={styles.title}>Details</h2>
           <div className={styles.formItem}>
               <label className={styles.fieldLabel} htmlFor="website"><span className={styles.asterisk}>*</span> website</label>
               <Input
                   id="website"
                   name="website"
                   placeholder="https://company.com"
                   value={formData.website}
                   onChange={(event) => {
                       handleFieldChange("website", event.target.value, true)
                   }}
                   className={cx(styles.input, {
                       [styles.inputError]: validationError.includes("website"),
                   })}
               />
           </div>
           <div className={cx(styles.formItem)}>
               <label className={styles.fieldLabel} htmlFor="address"><span className={styles.asterisk}>*</span> address</label >
               <Input
                   id="address"
                   name="address"
                   placeholder="Address"
                   value={formData.address}
                   onChange={(event) => {
                       handleFieldChange("address", event.target.value, true)
                   }}
                   className={cx(styles.input, {
                       [styles.inputError]: validationError.includes("address"),
                   })}
               />
          </div>
</div>
   )
}
export default SecondStep
Code language: JavaScript (javascript)

ThirdStep.js

import React from 'react'
import cx from "classnames";
import { Input } from 'reactstrap';
import styles from './Form.module.scss'
const ThirdStep = ({ validationError, formData, handleFieldChange }) => {
   return (
       <div className={styles.container}>
           <h2 className={styles.title}>Additional</h2>
           <div className={styles.formItem}>
               <label className={styles.fieldLabel} htmlFor="email"><span className={styles.asterisk}>*</span> email</label>
               <Input
                   id="email"
                   name="email"
                   placeholder="name@company.com"
                   value={formData.email}
                   onChange={(event) => {
                       handleFieldChange("email", event.target.value, true)
                   }}
                   className={cx(styles.input, {
                       [styles.inputError]: validationError.includes("email"),
                   })} />
           </div>
           <div className={cx(styles.formItem)}>
               <label className={styles.fieldLabel} htmlFor="foundationDate">Foundation date </label>
               <Input
                   id="foundationDate"
                   name="foundationDate"
                   placeholder="10/24/2015 (MM/DD/YYYY)"
                   value={formData.foundationDate}
                   onChange={(event) => {
                       handleFieldChange("foundationDate", event.target.value)
                   }}
                   className={styles.input}
               />
           </div>
       </div>
   )
}
export default ThirdStep
Code language: JavaScript (javascript)

Creating the StepsController component

The StepsController component handles navigation between steps. It controls the steps’ state and manages navigation between them. It receives the steps components, a function to check if a step is valid, the number of steps, and the form title as props. This structure allows us to reuse StepsController in different forms.

StepsController.js

import React, { useState } from "react";
import Button from "../../components/Button/Button"
import StepsIndicator from "./StepsIndicator";
import styles from "./StepsController.module.scss";


const StepsController = ({ steps, manageNextStepValidation, stepsAmount, formTitle }) => {
   const [step, setStep] = useState(1);
   const onNextStep = () => {
       if (manageNextStepValidation(step) && step !== stepsAmount) {
           setStep(step + 1)
       }
   }
   return (
       <div className={styles.container}>
           <div className={styles.indicatorContainer}>
               <h1 className={styles.title}>{formTitle}</h1>
               <StepsIndicator step={step} stepsAmount={stepsAmount} />
           </div>
           <div className={styles.formContainer}>
               <div> {
                   steps[step - 1]
               }</div>
               <div className={styles.buttonsContainer}>
                   <Button className={styles.nextButton} onClick={() => onNextStep()}>{step !== stepsAmount ? "Next" : "Send"}</Button>
                   {step !== 1 && <Button className={styles.backButton} onClick={() => setStep(step - 1)}>Back</Button>}
               </div>
           </div>
       </div>
   )
};
export default StepsController;
Code language: JavaScript (javascript)

StepIndicator.js

The StepsIndicator component displays the current step to the user. It is a stateless component that receives the current step and the total number of steps as props. In this example, the steps indicator is displayed only for screens wider than 872px. For mobile screens, you might want to change the design and possibly use a different steps indicator component.

import React from "react";
import cx from "classnames";
import Icon from "../../components/Icon/Icon";
import CheckedIcon from '../../icons/checked.svg'
import styles from "./StepsController.module.scss";


const StepsIndicator = ({ step, stepsAmount }) => {
   const getStepsIndicator = () => {
       const stepsAmountArray = []
       for (let i = 1; i <= stepsAmount; i++) {
           stepsAmountArray.push(i)
       }
       return stepsAmountArray
   }
   return (
       <div className={styles.stepsContainer}>
           {getStepsIndicator().map(item => <div className={styles.step} key={item}>
               <div className={styles.circleContainer} >
                   {item > 1 && <div className={cx(styles.stem, {
                       [styles.stemActive]: item === step || step > item
                   })}></div>}
                   <div className={cx(styles.circle, {
                       [styles.circleActive]: item === step || step > item
                   })}>
                       {step > item ? <Icon name={CheckedIcon} /> : <div className={cx(styles.circleIn, {
                           [styles.circleInActive]: item === step || step > item
                       })}></div>}
                   </div>
               </div>
           </div>
           )}
       </div>
   )
};
export default StepsIndicator;
Code language: JavaScript (javascript)

Form validation

In this example, custom form validation is used for more flexibility and control. You can also use packages like Formik or Yup to validate the form.

manageValidation.js

const validateEmail = (email) => {
 const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
 return re.test(String(email).toLowerCase());
}
const getErrors = (state, keys) => {
 return Object.entries(state)
   .filter(([key]) => keys.includes(key))
   .filter(([key, value]) =>
     key === "email" ? !validateEmail(value) : !value?.length
   )
   .map(([key]) => key);
}
const cancelValidationError = (
 filterType,
 setValidationError,
 validationError
) => {


 setValidationError(
   validationError.filter((errorItem) => errorItem !== filterType)
 );
};
const checkIsValidFormSteps = ({ formData, step }) => {
 const {
   name,
   address,
   website,
   email
 } = formData;


 if (step === 1) {
   return !!(name)
 }
 if (step === 2) {
   return !!(address && website)
 }
 if (step === 3) {
   return !!(validateEmail(email))
 }
}
const updateErrorsFormSteps = ({ formData, setValidationError, step }) => {
 if (step === 1) {
   const errors = getErrors(formData, ["name"]);
   setValidationError(errors);
 }
 if (step === 2) {
   const errors = getErrors(formData, ["website", "address"]);
   setValidationError(errors);
 }
 if (step === 3) {
   const errors = getErrors(formData, ["email"]);
   setValidationError(errors);
 }
};
export {
 cancelValidationError,
 checkIsValidFormSteps,
 updateErrorsFormSteps
};
Code language: JavaScript (javascript)

Conclusion

By following these steps, you’ve created a reusable multiple-step form in ReactJS. This approach allows you to manage form data efficiently, navigate between form steps, and enhance the user experience by breaking down complex forms into more straightforward, manageable parts.

You can find the full code on GitHub.

Related Posts

Native CSS: A Whole New Story – Part 1

Daniele Carta
March 3, 2025

Understanding Angular — Exploring Dependency Injection and Design Patterns — Part 0 🔥🚀

Giorgio Galassi
February 5, 2025

Let’s Create a Bento Box Design Layout Using Modern CSS

Massimo Avvisati
January 21, 2025
excalidraw codemotion magazine

Excalidraw: Diagrams Set in Stone

TheZal
July 9, 2024
Share on:facebooktwitterlinkedinreddit

Tagged as:form guide React tutorial

Noa Shtang
The “Hardcore” Attitude of Programmers
Previous Post
Got the Time: How a Dev Organizes Their Day While Trying to Stay Sane
Next Post

Footer

Discover

  • Events
  • Community
  • Partners
  • Become a partner
  • Hackathons

Magazine

  • Tech articles

Talent

  • Discover talent
  • Jobs

Companies

  • Discover companies

For Business

  • Codemotion for companies

About

  • About us
  • Become a contributor
  • Work with us
  • Contact us

Follow Us

© Copyright Codemotion srl Via Marsala, 29/H, 00185 Roma P.IVA 12392791005 | Privacy policy | Terms and conditions