React - Advanced Forms (Multi-step & Validation)
Overview
Estimated time: 30–45 minutes
Create a multi-step form wizard with per-step validation, live field feedback, and a final review + submit step. This mirrors common production patterns for onboarding and checkout flows.
Learning Objectives
- Build multi-step navigation with persistent form state across steps.
- Validate fields on change/blur and on step submission.
- Provide a review step and finalize submission safely.
Prerequisites
- Controlled inputs and basic validation.
Try it: Multi-step Wizard
View source
const { useState } = React;
function required(x){ return x ? '' : 'This field is required'; }
function emailRule(x){ if (!x) return 'Email is required'; return /.+@.+\..+/.test(x) ? '' : 'Enter a valid email'; }
function MultiStep(){
const [step, setStep] = useState(1);
const [data, setData] = useState({ name:'', email:'', address:'', city:'' });
const [errors, setErrors] = useState({});
function setField(key, val, rule){
setData(d => ({ ...d, [key]: val }));
if (rule){ setErrors(e => ({ ...e, [key]: rule(val) })); }
}
function validateStep(s){
const e = {};
if (s===1){ e.name = required(data.name); e.email = emailRule(data.email); }
if (s===2){ e.address = required(data.address); e.city = required(data.city); }
Object.keys(e).forEach(k => e[k] === '' && delete e[k]);
setErrors(prev => ({ ...prev, ...e }));
return Object.keys(e).length === 0;
}
function next(){ if (validateStep(step)) setStep(step+1); }
function back(){ setStep(step-1); }
async function submit(){
if (!validateStep(2)) { setStep(2); return; }
alert('Submitted: ' + JSON.stringify(data, null, 2));
}
return (
<div>
<div style={{marginBottom:8}}>Step {step} of 3</div>
{step===1 && (
<div>
<label>Name: <input value={data.name} onChange={e => setField('name', e.target.value, required)} onBlur={() => setField('name', data.name, required)} /></label>
{errors.name && <div style={{color:'salmon'}}>{errors.name}</div>}
<br />
<label>Email: <input value={data.email} onChange={e => setField('email', e.target.value, emailRule)} onBlur={() => setField('email', data.email, emailRule)} /></label>
{errors.email && <div style={{color:'salmon'}}>{errors.email}</div>}
<div style={{marginTop:8}}>
<button onClick={next}>Next</button>
</div>
</div>
)}
{step===2 && (
<div>
<label>Address: <input value={data.address} onChange={e => setField('address', e.target.value, required)} onBlur={() => setField('address', data.address, required)} /></label>
{errors.address && <div style={{color:'salmon'}}>{errors.address}</div>}
<br />
<label>City: <input value={data.city} onChange={e => setField('city', e.target.value, required)} onBlur={() => setField('city', data.city, required)} /></label>
{errors.city && <div style={{color:'salmon'}}>{errors.city}</div>}
<div style={{marginTop:8}}>
<button onClick={back}>Back</button>
<button onClick={next} style={{marginLeft:8}}>Next</button>
</div>
</div>
)}
{step===3 && (
<div>
<h4>Review</h4>
<pre style={{whiteSpace:'pre-wrap'}}>{JSON.stringify(data, null, 2)}</pre>
<div style={{marginTop:8}}>
<button onClick={back}>Back</button>
<button onClick={submit} style={{marginLeft:8}}>Submit</button>
</div>
</div>
)}
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-forms-adv')).render(<MultiStep />);
Syntax primer
- Wizard state: store a
step
index and a singledata
object. - Validation helpers return an empty string for OK and a message for errors.
- Validate on change/blur for live feedback and on step submit for safety.
Common pitfalls
- Resetting step-local state on navigation—persist it in a parent component.
- Blocking navigation without telling the user why—show inline error messages.
Exercises
- Add a phone field with formatting and validation.
- Persist form state to localStorage and restore it on reload.
Checks for Understanding
- When should you validate on change/blur vs on submit?
- Where should you store multi-step form state and why?
- What user feedback should be provided when a step is blocked due to validation?
- How do you ensure validation errors don’t linger once fields are corrected?
Answers
- Use on change/blur for immediate feedback and on submit/step change as a safety gate. Combining both gives responsive UX and prevents proceeding with invalid data.
- In a parent component (or a single state object) that persists across steps so that navigating back and forth doesn’t reset fields.
- Inline error messages near the fields, and clear, actionable messaging on the blocked action (e.g., disable Next or show which fields need attention).
- Re-run the validation rule when the field changes/blur and clear the error when the rule passes (e.g., return empty string or remove the key).
Checks for Understanding
- When should you validate on change/blur vs on submit?
- Where should you store multi-step form state and why?
- What user feedback should be provided when a step is blocked due to validation?
- How do you ensure validation errors don’t linger once fields are corrected?
Answers
- Use on change/blur for immediate feedback and on submit/step change as a safety gate. Combining both gives responsive UX and prevents proceeding with invalid data.
- In a parent component (or a single state object) that persists across steps so that navigating back and forth doesn’t reset fields.
- Inline error messages near the fields, and clear, actionable messaging on the blocked action (e.g., disable Next or show which fields need attention).
- Re-run the validation rule when the field changes/blur and clear the error when the rule passes (e.g., return empty string or remove the key).