React Curriculum

Machine Coding: Multi-Step Wizard with Step Validation

Hard100% Free~40 mins#react#machine-coding#multi-step-form#wizard#validation#forms
Interview Answer (30-Second Summary)

A production Multi-Step Form Wizard manages step navigation as a finite state machine, persists accumulated form data across steps, isolates field validation per step, supports backward navigation without data loss, and submits a consolidated payload upon final review.

Key Learning Objectives
✓

Architect a multi-step wizard supporting dynamic step transitions and review summaries.

✓

Validate form fields on a per-step basis before permitting advancement to subsequent steps.

✓

Preserve form draft data across forward and backward step navigation.

✓

Build step progress indicators with accessible ARIA progress contracts.

The Interview Problem & Context

Implement a production 3-step Wizard Form (Account -> Profile -> Review & Submit) with step validation, data persistence, and backward navigation.

1import React, { useState } from 'react';
2
3interface FormData {
4 email: string;
5 username: string;
6 role: string;
7 acceptTerms: boolean;
8}
9
10const INITIAL_DATA: FormData = {
11 email: '',
12 username: '',
13 role: 'developer',
14 acceptTerms: false,
15};
16
17export function MultiStepWizard({ onSubmit }: { onSubmit: (data: FormData) => void }) {
18 const [currentStep, setCurrentStep] = useState(0);
19 const [formData, setFormData] = useState<FormData>(INITIAL_DATA);
20 const [errors, setErrors] = useState<Record<string, string>>({});
21
22 const updateFields = (fields: Partial<FormData>) => {
23 setFormData((prev) => ({ ...prev, ...fields }));
24 setErrors({}); // Clear errors on change
25 };
26
27 // Step 1 Validation: Account Info
28 const validateStep1 = () => {
29 const errs: Record<string, string> = {};
30 if (!formData.email.includes('@')) errs.email = 'Valid email is required';
31 setErrors(errs);
32 return Object.keys(errs).length === 0;
33 };
34
35 // Step 2 Validation: Profile Info
36 const validateStep2 = () => {
37 const errs: Record<string, string> = {};
38 if (formData.username.trim().length < 3) errs.username = 'Username must be at least 3 chars';
39 setErrors(errs);
40 return Object.keys(errs).length === 0;
41 };
42
43 const handleNext = () => {
44 if (currentStep === 0 && !validateStep1()) return;
45 if (currentStep === 1 && !validateStep2()) return;
46 setCurrentStep((prev) => prev + 1);
47 };
48
49 const handleBack = () => {
50 setErrors({});
51 setCurrentStep((prev) => Math.max(0, prev - 1));
52 };
53
54 const handleSubmit = (e: React.FormEvent) => {
55 e.preventDefault();
56 if (!formData.acceptTerms) {
57 setErrors({ terms: 'Must accept terms to proceed' });
58 return;
59 }
60 onSubmit(formData);
61 };
62
63 const steps = ['Account', 'Profile', 'Review'];
64
65 return (
66 <div className="wizard-container" style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
67 {/* Progress Indicator */}
68 <nav aria-label="Form Progress" style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '24px' }}>
69 {steps.map((label, idx) => (
70 <div key={label} style={{ fontWeight: currentStep === idx ? 'bold' : 'normal' }}>
71 Step {idx + 1}: {label}
72 </div>
73 ))}
74 </nav>
75
76 {/* Step 1: Account Info */}
77 {currentStep === 0 && (
78 <div className="step-panel">
79 <h3>Account Information</h3>
80 <label>Email Address</label>
81 <input
82 type="email"
83 value={formData.email}
84 onChange={(e) => updateFields({ email: e.target.value })}
85 style={{ display: 'block', width: '100%', marginBottom: '8px' }}
86 />
87 {errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
88 </div>
89 )}
90
91 {/* Step 2: Profile Info */}
92 {currentStep === 1 && (
93 <div className="step-panel">
94 <h3>Profile Setup</h3>
95 <label>Username</label>
96 <input
97 type="text"
98 value={formData.username}
99 onChange={(e) => updateFields({ username: e.target.value })}
100 style={{ display: 'block', width: '100%', marginBottom: '8px' }}
101 />
102 {errors.username && <p style={{ color: 'red' }}>{errors.username}</p>}
103
104 <label>Primary Role</label>
105 <select
106 value={formData.role}
107 onChange={(e) => updateFields({ role: e.target.value })}
108 style={{ display: 'block', width: '100%', marginTop: '8px' }}
109 >
110 <option value="developer">Developer</option>
111 <option value="designer">Designer</option>
112 <option value="manager">Product Manager</option>
113 </select>
114 </div>
115 )}
116
117 {/* Step 3: Review & Submit */}
118 {currentStep === 2 && (
119 <div className="step-panel">
120 <h3>Review & Confirm</h3>
121 <p>Email: {formData.email}</p>
122 <p>Username: {formData.username}</p>
123 <p>Role: {formData.role}</p>
124
125 <label>
126 <input
127 type="checkbox"
128 checked={formData.acceptTerms}
129 onChange={(e) => updateFields({ acceptTerms: e.target.checked })}
130 />{' '}
131 I accept the terms and conditions
132 </label>
133 {errors.terms && <p style={{ color: 'red' }}>{errors.terms}</p>}
134 </div>
135 )}
136
137 {/* Navigation Buttons */}
138 <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px' }}>
139 {currentStep > 0 && (
140 <button type="button" onClick={handleBack}>
141 Back
142 </button>
143 )}
144 {currentStep < 2 ? (
145 <button type="button" onClick={handleNext}>
146 Next Step
147 </button>
148 ) : (
149 <button type="button" onClick={handleSubmit}>
150 Submit Registration
151 </button>
152 )}
153 </div>
154 </div>
155 );
156}
Deep Architectural Explanation

Architectural Decisions in Multi-Step Form Wizards

1. Centralized vs Distributed State:

  • Distributed State (Anti-pattern): Storing email in Step 1 and username in Step 2 causes state loss whenever steps unmount!
  • Consolidated State (Recommended): The parent <MultiStepWizard> owns the authoritative formData state. Steps receive data and callbacks via props or context.
  • Moving between steps merely mounts different UI views while the underlying accumulated state persists untouched.

2. Isolated Step Validation:

  • Validating the entire form on step 1 would flag step 2 and step 3 fields as empty errors.
  • Therefore, each step executes its own scoped validator before calling setCurrentStep(prev => prev + 1).

3. State Machine Modeling:

  • Steps represent states in a finite state machine:
    [Account] -(Valid)-> [Profile] -(Valid)-> [Review] -(Submit)-> [Complete]
  • Invalid inputs cannot trigger state transitions.
  • Backward transitions are always permitted and never require validation.
Common Interview Traps & Pitfalls
✕

Storing form state inside individual step components, losing user data whenever the step unmounts on Back or Next.

✕

Validating future step fields prematurely, confusing users with errors on inputs they have not seen yet.

✕

Allowing step advancement via URL or buttons without running step validation checks.

Senior Interviewer Follow-Up Probes
?

How does React Hook Form or Zod integrate with multi-step schemas using `z.discriminatedUnion`?

?

How would you allow users to jump directly to any previously completed step via the breadcrumb indicator?

?

How do you prevent duplicate form submissions on network latency in the final submit step?

Frequently Asked Interview Questions
Where should state be stored in a multi-step form wizard?

In the parent wizard component or an external store. Storing state inside individual step components causes data to be lost when steps unmount.

Validate only the fields belonging to the current active step before allowing the user to advance to the next step, avoiding premature validation errors on future fields.

Because all form data is retained in the parent component state, moving backward simply renders the previous step panel with existing state pre-populated.

Yes. Storing ?step=2 in the URL allows browser back/forward buttons to navigate between steps naturally and enables deep linking.

Save the formData and active step in sessionStorage on each change, and initialize state from sessionStorage on component mount.

Compute the active steps array dynamically based on user selections in previous steps (e.g., if user selects "Business", inject a "Company Details" step).