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