Back to Blog
Lesson 31 of the Intermediate React: Hooks, State & Data Patterns course
ReactJune 26, 20263 min read

Handling Multi-step Forms: Building Robust Wizards in React

Master multi-step forms by centralizing state and managing wizard navigation. Learn to persist data across steps for a seamless dashboard configuration UX.

ReactFormsState ManagementUXWizardsjavascriptfrontend

Previously in this course, we explored schema-based validation with Zod to ensure our form inputs are type-safe and clean. Now, we're taking that foundation to the next level by architecting multi-step forms.

When building complex dashboard configurations, you rarely want to overwhelm the user with a single page containing dozens of fields. Breaking these into a "wizard" pattern improves UX, but introduces a new challenge: how do we maintain state across multiple components?

The Architecture of Wizards

In a multi-step form, the "source of truth" must live outside the individual step components. If you keep the state inside each step, you lose the data as soon as the user navigates away.

We need a strategy that:

  1. Centralizes State: A parent container (or context) holds the entire form data object.
  2. Handles Navigation: A local state tracks the current step index.
  3. Persists Progress: The parent passes down the current data and an update function to each step.

Worked Example: Building the Dashboard Config Wizard

Let's build a three-step wizard for our dashboard settings: Profile, Preferences, and Review.

JSX
import { useState } from CE9178">'react';

const Wizard = () => {
  const [step, setStep] = useState(0);
  const [formData, setFormData] = useState({
    username: CE9178">'',
    theme: CE9178">'light',
    notifications: true,
  });

  const updateData = (newData) => {
    setFormData((prev) => ({ ...prev, ...newData }));
  };

  const next = () => setStep((s) => s + 1);
  const prev = () => setStep((s) => s - 1);

  return (
    <div className="wizard">
      {step === 0 && <ProfileStep data={formData} onUpdate={updateData} />}
      {step === 1 && <PreferencesStep data={formData} onUpdate={updateData} />}
      {step === 2 && <ReviewStep data={formData} />}
      
      <div className="controls">
        {step > 0 && <button onClick={prev}>Back</button>}
        {step < 2 && <button onClick={next}>Next</button>}
        {step === 2 && <button onClick={() => console.log(formData)}>Submit</button>}
      </div>
    </div>
  );
};

By lifting the state up, we ensure that whether the user is on the first or third step, the formData object remains intact. This is a critical pattern for robust controlled vs uncontrolled components workflows.

Hands-on Exercise: Implementing Step Persistence

  1. Create a useFormState custom hook: Extract the formData and updateData logic into a hook so your Wizard component doesn't get cluttered.
  2. Add Validation: Integrate the Zod logic from our previous lesson to ensure that clicking "Next" only advances the state if the current step's data is valid.
  3. Persist to LocalStorage: Use your useLocalStorage logic to ensure that if a user refreshes the page mid-wizard, their progress is saved.

Common Pitfalls

  • Losing State on Unmount: Never define your formData inside a step component. If you move from StepA to StepB, StepA unmounts and its local state is destroyed. Always lift it to the parent.
  • Over-complicating with Context: You don't always need Context for a wizard. If the wizard exists on only one page, pass props down. Only reach for Context if you need to access this form state from deeply nested components or multiple parts of the application.
  • Ignoring the "Back" Button: Users expect to go back and see their previous inputs. Ensure your inputs are controlled components that receive their value from the central formData state.

Recap

Managing multi-step forms effectively boils down to state placement. By keeping data in a parent component, you treat the entire wizard as a single, cohesive unit of data. This prevents data loss, simplifies validation, and provides a predictable user experience for complex dashboard configurations.

Up next: We will discuss how to optimize these form submissions by handling loading states and server-side errors, ensuring your users get immediate feedback when they hit that final "Submit" button.

Similar Posts