Introduction
Multi-step forms, often referred to as wizards, divide the user journey into a series of steps. Each step is designed to gather specific information from the user, like personal details, contact data, or preferences. By partitioning the registration or data entry process into smaller, more manageable sections, multi-step forms improve the overall user experience and make data collection more efficient.
Implementing a multi-step form or wizard design is frequently regarded as a best practice, especially for intricate processes or forms requiring multiple stages. Here are several reasons why this approach is often seen as advantageous:
Improved Usability: Breaking down a long form into smaller sections makes it less intimidating and easier for users to complete.
Enhanced User Experience: Users can focus on one task at a time, reducing cognitive load and preventing overwhelm.
Higher Completion Rates: Users are more likely to complete shorter, segmented forms, leading to better data collection.
Clear Progress Indicators: Showing users their progress through steps helps keep them motivated and informed.
Error Reduction: Focusing on one piece of information at a time reduces the likelihood of mistakes.
Better Data Management: Segmented data collection allows for easier validation and management at each step.
In this guide, we'll delve into creating a multi-step form in React, showcasing how to effectively implement this pattern to enhance your application's user experience and data collection efficiency.
Setting up the project
Run the following command in the terminal to create and set up the Next.js application:
npx create-next-app@latest my-registration-app --ts
Navigate in the project folder using the following command:
cd my-registration-app
In the src
directory, create a components folder and in it, create Details.tsx
, Organization.tsx
and Verification.tsx
files. Also, in the components folder, create a parent RegistrationForm.tsx
to manage the registration flow.
Developing Details component
In this first step, we're going to develop it with three fields: email, Password and Re-enter password.
Paste the code below into Details.tsx
file:
import React, { useState, ChangeEvent } from 'react';
// Define the props interface for Step1 component
interface DetailsProps {
onNext: () => void; // Function to move to the next step
onChange: (data: { [key: string]: string }) => void; // Function to handle form data changes
}
const Details: React.FC<DetailsProps> = ({ onNext, onChange }) => {
// State to manage form data
const [formData, setFormData] = useState({
email: '',
password: '',
reEnterPassword: '',
});
// Function to handle input changes
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
// Update the form data state
setFormData({ ...formData, [name]: value });
// Invoke the onChange function with updated form data
onChange({ [name]: value });
};
return (
<div>
{/* Input fields for email, password, and re-enter password */}
<input type="email" name="email" placeholder="Email" value={formData.email} onChange={handleChange} />
<input type="password" name="password" placeholder="Password" value={formData.password} onChange={handleChange} />
<input type="password" name="reEnterPassword" placeholder="Re-enter Password" value={formData.reEnterPassword} onChange={handleChange} />
{/* Button to move to the next step */}
<button onClick={onNext}>Get Started</button>
</div>
);
};
export default Details;
In the code above, we use the local state (formData
) to handle input values and update this state via the handleChange
function, which also calls onChange
to pass updated data to the parent component. The onNext
function, triggered by the "Get Started" button, advances to the next form step. The component receives onNext
and onChange
as props for these purposes.
Developing organization details
Let's now develop the second part of our wizard, which is a form to collect users' organization details.
Paste the below code into the Organization.tsx
file:
import React, { useState, ChangeEvent } from 'react';
interface OrganizationProps {
onNext: () => void;
onPrevious: () => void;
onChange: (data: { [key: string]: string }) => void;
}
const Organization: React.FC<OrganizationProps> = ({ onNext, onPrevious, onChange }) => {
const [formData, setFormData] = useState({
organization: '',
phoneNumber: '',
message: '',
});
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
onChange({ [name]: value });
};
return (
<div className="flex flex-col gap-4">
<p className="text-lg font-semibold">Step 2:</p>
<input type="text" name="organization" placeholder="Organization" value={formData.organization} onChange={handleChange} className="border border-black rounded outline-none px-4 py-2" style={{ color: 'black' }} />
<input type="tel" name="phoneNumber" placeholder="Phone Number" value={formData.phoneNumber} onChange={handleChange} className="border border-black rounded outline-none px-4 py-2" style={{ color: 'black' }} />
<textarea name="message" placeholder="Your Message" value={formData.message} onChange={handleChange} className="border border-black rounded outline-none px-4 py-2" style={{ color: 'black' }}/>
<button className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded" onClick={onPrevious}> Back </button>
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={onNext}>Create Account</button>
</div>
);
};
export default Organization;
The above code manages the second step of our multi-step form, collecting the organization name, phone number, and a message. It uses local state (formData
) to track input values, updated via the handleChange
function, which also triggers the onChange
prop to pass data to the parent component. We include the "Back" and "Create Account" buttons to trigger the onPrevious
and onNext
functions, respectively, to navigate between form steps.
Developing verification details component
Our last step is to develop the final part of our multi-step form, which is to verify the user's email address.
To do this, copy and paste the code below into the verification.tsx file:
import React from 'react';
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" />
interface VerificationProps {
onPrevious: () => void;
}
const Verification: React.FC<VerificationProps> = ({ onPrevious }) => {
return (
<div className="flex flex-col gap-4">
<p className="text-lg font-semibold">Step 3:</p>
<h2 className="text-lg font-semibold text-center">Check Your Email</h2>
<p className="text-lg font-semibold">We sent you an email with further instructions.</p>
<button className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded" onClick={onPrevious}> Back </button>
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Open Email App</button>
</div>
);
};
export default Verification;
The code above represents the third step in our multi-step form, informing the user to check their email for further instructions. It includes a "Back" button that triggers the onPrevious
function to navigate to the previous step and an "Open Email App" button for user convenience.
Developing Registration form flow
This is the parent component that is going to manage the flow of registration.
Copy and paste the code below in the RegistrationForm.tsx
file:
import React, { useState } from 'react';
import Details from './Details';
import Organization from './Organization';
import Verification from './Verification';
const RegistrationFlow = () => {
// State to track the current step of the registration flow
const [step, setStep] = useState(1);
// State to store form data across steps
const [formData, setFormData] = useState({});
// Function to move to the next step
const handleNextStep = () => {
setStep(step + 1);
};
// Function to move to the previous step
const handlePreviousStep = () => {
setStep(step - 1);
};
// Function to handle form data changes across steps
const handleFormDataChange = (data: { [key: string]: string }) => {
setFormData({ ...formData, ...data });
};
return (
<div>
{/* Render Step1 component if step is 1 */}
{step === 1 && <Details onNext={handleNextStep} onChange={handleFormDataChange} />}
{/* Render Step2 component if step is 2 */}
{step === 2 && <Organization onNext={handleNextStep} onPrevious={handlePreviousStep} onChange={handleFormDataChange} />}
{/* Render Step3 component if step is 3 */}
{step === 3 && <Verification onPrevious={handlePreviousStep} />}
</div>
);
};
export default RegistrationFlow;
The RegistrationFlow
component manages a multi-step registration process by controlling navigation between different steps (Details
, Organization
, Verification
). It tracks the current step and stores form data while providing functions to move between steps and handle data changes.
Here's how it works:
Imports: We import the necessary modules from React, including
useState
the individual step components.State Management: We define two state variables:
step
to track the current step (initialized to 1) andformData
to store data across steps (initialized as an empty object).Navigation Functions:
handleNextStep
Increments thestep
state to move to the next step.handlePreviousStep
: Decrements thestep
state to move back to the previous step.handleFormDataChange
: Takes an object representing form data changes and merges it with the existingformData
state.
Conditional Rendering: Based on the current value of
step
, we render the appropriate component:If
step
is 1, render, pass,Details
and passhandleNextStep
andhandleFormDataChange
as props.If
step
is 2, renderOrganization
and passhandleNextStep
,handlePreviousStep
, andhandleFormDataChange
as props.If
step
is 3, renderVerification
and passhandlePreviousStep
as a prop.
Rendering Registration Form component
To render our registration form component, create a folder named registration
in the app directory. Then, create a file called page.tsx
inside this folder and paste the code below into it.
"use client"
import React from 'react';
import RegistrationForm from '@/components/RegistrationForm';
const RegistrationPage = () => {
return (
<div className="flex justify-center items-center h-screen">
<RegistrationForm />
</div>
);
};
export default RegistrationPage;
Update the layout.tsx
file with the code below
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
The code sets up a root layout by importing global CSS and a Google font and defines metadata. We wrap the app's children components in an HTML structure with the imported font applied to the body.
Also, update the global CSS file to style our application.
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
Lastly, update the app/page.tsx
file
import Link from 'next/link';
export default function Home() {
return (
<div className="flex justify-center items-center h-screen">
<Link href="/registration">
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Go to Registration
</button>
</Link>
</div>
);
}
To see the results of what we've built on the web server, run the following command in the terminal:
npm run dev
These commands will start the development server, and your project will be hosted at http://localhost:3000.
It will first display a "Go to Registration" button. When you click it, you will be taken to the first step of filling in your details, as shown below.
Conclusions
In conclusion, building a wizard form in React enhances the user experience by breaking down complex data entry processes into manageable steps. This step-by-step guide has shown you how to create a multi-step form, manage state across different stages, and handle form data efficiently. By following these principles, you can streamline user interactions and ensure a more intuitive and user-friendly interface. Happy coding!
If you found this guide helpful, please check out my full article for more details and follow me on Twitter and LinkedIn for more updates and insights!
🐦 [Twitter Handle]
💼 [LinkedIn Profile]