Master multi-step forms by centralizing state and managing wizard navigation. Learn to persist data across steps for a seamless dashboard configuration UX.
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?
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:
Let's build a three-step wizard for our dashboard settings: Profile, Preferences, and Review.
JSXimport { 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.
useFormState custom hook: Extract the formData and updateData logic into a hook so your Wizard component doesn't get cluttered.useLocalStorage logic to ensure that if a user refreshes the page mid-wizard, their progress is saved.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.formData state.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.
Learn to optimize form submissions in React by disabling buttons during requests, handling server errors, and providing clear, actionable user feedback.
Read moreMaster dashboard data flow by auditing state synchronization and fixing stale data issues. Learn to bridge the gap between server state and your local UI.
Handling Multi-step Forms
Implementing Middleware for State
Advanced Context Patterns
Router Loaders and Data Prefetching
Complex Route Guards
Handling Large Datasets in UI
Testing Hooks and Components
Managing Global Modals
Implementing Keyboard Shortcuts
Optimizing Asset Loading
Internationalization Basics
Managing WebSocket Connections