Step-by-Step Guide to Building a Wizard Form in React

Step-by-Step Guide to Building a Wizard Form in React

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:

  1. Imports: We import the necessary modules from React, including useState the individual step components.

  2. State Management: We define two state variables: step to track the current step (initialized to 1) and formData to store data across steps (initialized as an empty object).

  3. Navigation Functions:

    • handleNextStepIncrements the step state to move to the next step.

    • handlePreviousStep: Decrements the step state to move back to the previous step.

    • handleFormDataChange: Takes an object representing form data changes and merges it with the existing formData state.

  4. Conditional Rendering: Based on the current value of step, we render the appropriate component:

    • If step is 1, render, pass, Details and pass handleNextStep and handleFormDataChange as props.

    • If step is 2, render Organization and pass handleNextStep, handlePreviousStep, and handleFormDataChange as props.

    • If step is 3, render Verification and pass handlePreviousStep 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]

Did you find this article valuable?

Support Daniel Musembi by becoming a sponsor. Any amount is appreciated!