Articles
Multi-Step Form Coding Interview: Validation, Back Navigation, and Draft State
Practice multi-step form coding interviews with dependent validation, back navigation, draft recovery, stale checks, and safe final submission boundaries.
A multi-step form coding interview is not just an array of screens and a Next button. The real challenge is keeping data trustworthy when users go back, change an earlier dependency, restore an old draft, or receive a late validation response for a value they no longer entered.
The key invariant is visited does not mean valid. Completion must describe current data under current rules, not the user's navigation history. Start with Implement a multi-step form in React. The contact/address/review workflow below is an original exercise. Official React, WAI, and MDN documentation supports the platform behavior; this is not a reported company's assessment format.
What must the wizard preserve, and what may change?
Define three steps: Contact, Address, and Review. Contact collects a name and email. Address collects country, postal code, and a shipping option. Review displays the applicable values and permits final submission. There are no passwords, payment credentials, or real orders in this exercise. Country changes both postal-code rules and available shipping options.
Ask whether Back saves edits, whether users can jump to future steps, whether drafts survive reload, and whether validation calls a server. Also ask what “complete” means. A checkmark can mean visited, passed validation at some point, or currently valid. Only the last meaning is safe to use as evidence for submission; if the UI displays navigation history, label it accordingly.
The WAI multi-page forms tutorial recommends logical grouping and clear progress information. Our implementation choice is to show “Step 2 of 3: Address,” preserve earlier entries, and make Back available without a validity gate. A user correcting a mistake should not have to invent a valid postal code just to reach Contact again.
Future-step access is a separate rule. We allow returning to visited steps, but reaching Review requires currently valid Contact and Address data. A bookmarked URL or a saved step number cannot bypass that check. Navigation is a request; the data model decides whether the destination is available.
Where should data live when a step unmounts?
Keep shared values in the wizard's durable state, outside the conditionally rendered step. Separate data, currentStep, touchedFields, errors, pendingChecks, and submissionStatus. Do not duplicate the address in both the Address component and Review component and hope an effect synchronizes them later.
React's state-lifetime guidance explains that state belongs to a component's position and identity in the rendered tree; removing a subtree can discard its local state. The practical implication for this exercise is to lift values above the step switch. Re-rendering a step should read the same wizard data rather than initialize another empty copy.
An input can still own temporary presentation details, such as whether a help tooltip is open. Those are not the source of truth for submission. Name fields by domain identity, not by their current screen index. If a step is inserted before Address, address.country should not become another field merely because it moved from index one to index two.
Choose when edits commit. Controlled inputs can update shared data on every edit. A step-local edit buffer can also work, but Back and Next must flush it consistently before navigating. State that boundary explicitly. A common bug is saving on Next only, so Back silently discards the latest correction even though the previous values remain visible elsewhere.
What should Next validate, and what should Submit validate?
Next validates the current step and any prerequisites needed for the destination. Back changes navigation without demanding validity. Final Submit validates every applicable field across all steps, regardless of which screens were visited or what old completion flags contain.
Keep validators pure where possible: validateAddress(data) returns field-keyed errors and does not navigate or send a request. Then an orchestration layer decides whether to stay, advance, or move to the earliest invalid step. This makes the same rules usable for Next, Review, restored drafts, and final submission.
The HTML constraint-validation guide distinguishes built-in checks from custom constraints and emphasizes server-side validation. Native required and email checks can support a visible step, but they cannot prove that an unmounted earlier step or a cross-field shipping rule is valid. Clear a custom validity message when its condition is corrected; an old message can otherwise keep blocking a field.
Use this original navigation contract:
| Action | Data handling | Validation / destination |
|---|---|---|
| Edit a field | Commit current value | Invalidate affected derived checks |
| Back | Preserve current edits | Move backward without a validation gate |
| Next | Keep all values | Advance only after current rules pass |
| Open Review | Derive current summary | Recheck required preceding steps |
| Submit | Build applicable payload | Validate all steps, then send once |
| Server rejects submission | Retain user input | Show actionable error at the owning step |
A disabled Submit button may prevent repeated clicks, but it is not the final correctness boundary. The server must enforce rules independently and handle repeated requests according to its own idempotency contract. In this local exercise, we test the client decision logic, not a real payment or order service.
How does an earlier edit invalidate later answers?
Consider a synthetic address: country US, postal code 94107, shipping option domestic-ground. The user reaches Review, returns to Address, and changes country to CA. The previous postal value and shipping choice still exist, but their earlier validation is no longer evidence for the new country.
Model dependencies explicitly: country → postal rules, country → shipping options, and both validated outputs feed Review. You may preserve the typed postal string for correction, but clear its valid status. Clear or flag a shipping choice that is absent from the new option set. Do not indiscriminately erase unrelated Contact values.
| Moment | Country / postal / shipping | What the wizard can honestly claim |
|---|---|---|
| Review before edit | US / 94107 / domestic-ground | Valid under the exercise's US rules |
| Country changes | CA / 94107 / old option | Address is visited, but not currently valid |
| Postal corrected | CA / M5V 2T6 / old option | Postal may pass; shipping still unresolved |
| Shipping replaced | CA / M5V 2T6 / eligible option | Address can pass current rules |
These postal values demonstrate dependency changes, not a complete international postal validation service. Real rules require a maintained specification and appropriate localization. The interview point is that rule identity changed even if the text field did not.
Completion can be derived by running validators over current applicable data. If server checks are expensive, cache results against a dependency key and version, not a permanent addressDone = true. A cache entry for (US, 94107) is not evidence for (CA, 94107). Explain the key before discussing memoization.
What prevents a stale validation response from winning?
Suppose country/postal revision seven starts a lookup. The user edits again, producing revision eight and a new lookup. Revision eight completes first; revision seven returns later. The old response must not replace the new result or unlock Review.
Give each request a ticket containing the current dependency revision and normalized input key. Apply a response only if both still match the current model. Canceling an older request is helpful for resource usage, but a correctness guard is still needed because cancellation may arrive too late or the operation may already have completed.
function acceptPostalResult(ticket, result, state) {
const currentKey = `${state.data.country}|${state.data.postal}`;
if (ticket.revision !== state.addressRevision) return false;
if (ticket.key !== currentKey) return false;
state.postalCheck = { key: currentKey, result };
return true;
}
Normalize the key consistently with the validation contract, not by applying arbitrary transformations. If spaces are meaningful to a rule, removing them just for the cache could merge distinct inputs. The snippet intentionally omits transport code: its job is to make response ownership visible and testable.
Represent pending, valid, invalid, and unavailable separately. A network timeout is not proof that the address is invalid. Decide whether final submission must wait for a mandatory check, offer retry, or let the server make the authoritative decision. Do not accidentally interpret “no error response yet” as “passed.”
How should restoring a draft work?
A draft is untrusted input, even when it came from this browser. Store a versioned, explicitly allowed data object rather than serializing the entire component state. On restore, parse safely, validate types, migrate a supported old version, copy allowed fields, and rerun current validators. Do not restore old errors, pending request handles, or completion flags as truth.
For this exercise, a version-two draft includes name, email, country, postal code, an allowed shipping value, and a requested step. It excludes password or card data and all submission credentials. If the requested step is no longer eligible, land on the earliest step requiring attention while preserving useful values.
MDN's localStorage reference documents origin-scoped string storage and access failures such as SecurityError. Treat reads and writes as fallible. Malformed JSON, unsupported versions, quota failure, or a blocked storage policy must not make the entire wizard unusable. Show a non-blocking “Draft could not be saved” message while retaining current in-memory values.
Debounce writes if needed, but define what happens before navigation or reload. A successful local save does not establish cross-device synchronization or conflict resolution. If server-backed drafts are added, introduce draft revisions and a merge policy deliberately; do not upload a stale full object over another device's edits without detecting the conflict.
How do errors and focus help users recover?
After Next fails, show concise field-specific errors and keep the user on the current step. After final validation fails on an earlier step, navigate there, render its controls, then focus a useful error summary or the first invalid field. Attempting to focus an input before that step mounts is a race, not a recovery strategy.
The WAI notification guidance supports clear overall feedback, links to affected controls, and associating inline messages with fields. Our chosen UI uses labeled inputs, aria-invalid where applicable, and error descriptions connected to the corresponding controls. Errors explain a correction, rather than merely turning a border red.
Do not announce a new alert for every partial keystroke. Choose validation timing deliberately: on blur for format feedback, on Next for step progression, and on Submit for whole-form confirmation. Asynchronous checks can expose a polite pending or result status without repeatedly stealing focus.
A successful submission needs visible confirmation, not only a disabled button or a silent network response. A failed submission preserves the draft and distinguishes server rejection from transport uncertainty. If a retry could duplicate a business operation, the server contract must resolve that ambiguity; the client cannot infer that nothing happened from a timeout alone.
What tests reveal the hidden bugs?
Write a pure-model test that reaches Review, returns to Address, changes country, and attempts submission with the old postal/shipping combination. It must fail despite every step having been visited. Then correct one dependency at a time to show which condition is still missing.
Test revision eight followed by late revision seven; malformed and unsupported drafts; storage exceptions; hidden conditional fields; double submission; and an error owned by an unmounted step. For conditional fields, both validation and payload construction must use the same applicability rule. A hidden field should not secretly block progress or leak a value the user thought was removed.
Our local browser fixture also verifies that Back preserves typed values and that changing country invalidates Review eligibility. These bounded tests do not prove a production server, storage durability, every locale's address rules, or a full assistive-technology matrix. Explain precisely which layer each test observes.
Finish with the invariant rather than a framework slogan: “The draft stores values, not proof of validity. Final submission validates the current applicable data, and old async responses cannot change that decision.” That answer scales beyond three screens because it identifies the trust boundary.