How would you manage navigation between steps and handle errors or validation for each step
Track currentStep in state; gate 'Next' on validating the current step's slice. Keep per-step errors in shared state, allow free Back navigation, mark steps visited/valid, sync step to the URL for deep-linking and refresh, and block final submit until all steps are valid.
The companion to "where does the data live" — this is how the user moves through the wizard and how validation gates that movement.
Navigation state
const [currentStep, setCurrentStep] = useState(0);
const [errors, setErrors] = useState({}); // keyed by step
const [visited, setVisited] = useState({ 0: true });Validation gates "Next" — not "Back"
- Going forward — validate the current step's slice before advancing. If it fails, set that step's errors and stay put.
- Going back — always allowed freely; never make a user re-pass validation to look at a previous step.
const next = () => {
const stepErrors = validateStep(currentStep, formData);
if (Object.keys(stepErrors).length) {
setErrors((e) => ({ ...e, [currentStep]: stepErrors }));
return; // blocked
}
setVisited((v) => ({ ...v, [currentStep + 1]: true }));
setCurrentStep((s) => s + 1);
};
const back = () => setCurrentStep((s) => Math.max(0, s - 1));Error handling per step
- Keep errors in shared state keyed by step (not local to a step component, which unmounts).
- Show field-level errors inline; optionally a step-level summary.
- A stepper/progress UI that marks each step as untouched / valid / has-errors, so the user can see where problems are.
- Let users jump to a step via the stepper only if intermediate steps are valid (or only to visited steps).
The details that signal seniority
- Sync
currentStepto the URL (/wizard/step-2or?step=2) — makes the browser back button work intuitively, allows deep-linking, and survives refresh (paired with persisted data). - **Final submit is gated on all steps valid** — re-run every step's validation before submitting; jump the user to the first invalid step if something's wrong.
- Re-validate on back-edits — if editing step 1 invalidates step 3, mark step 3 dirty.
- Unsaved-changes guard — warn before navigating away from a partially-filled wizard.
- Focus management & accessibility — move focus to the new step's heading on navigation; announce step changes.
The framing
"I track currentStep plus per-step errors and a visited map in the shared state. 'Next' is gated — validate the current step's slice, and only advance if it passes, otherwise surface that step's errors. 'Back' is always free. A stepper UI shows which steps are valid or have errors. The senior details: sync the step to the URL so the browser back button and refresh work, gate the final submit on all steps validating — jumping to the first invalid one — and handle focus and an unsaved-changes guard."
Follow-up questions
- •Why allow free Back navigation but gate Next?
- •Why sync the current step to the URL?
- •How do you handle a back-edit that invalidates a later step?
- •How do you prevent the final submit when an earlier step is invalid?
Common mistakes
- •Letting users advance past an invalid step.
- •Blocking Back navigation behind validation.
- •Keeping step errors in local step state that unmounts.
- •Not syncing step to the URL — broken back button and lost progress on refresh.
- •Not re-validating everything before final submit.
Performance considerations
- •Navigation is cheap; the cost is re-rendering. Memoize step components so only the active step renders, and keep validation per-slice rather than revalidating the whole form on every keystroke.
Edge cases
- •Editing an earlier step invalidates a later one.
- •User deep-links to step 3 without completing steps 1–2.
- •Browser back button vs. in-app navigation.
- •Refresh mid-wizard.
Real-world examples
- •Checkout wizards with a progress stepper and per-step validation.
- •Onboarding flows where the step is in the URL for resumability.