How do you structure one form model across several steps so each step validates its own subset and going back keeps state?
answer
- one model, several views over it
- step membership is data, not markup
- validate a subset to advance
- two gates: step and whole shape
- going back re-renders, never re-seeds
basics
~20 sKeep one model for the whole form and declare each step's field paths as data. Validate only that subset to advance, keep the whole model alive so going back restores what was typed, and validate the complete shape again before submitting.
solid answer
~40 sTreat the steps as **views over one model**, not as separate forms. One model is what makes cross-step rules expressible and what makes going back a re-render rather than a restore. Then declare each step's membership as **data** — paths, prefixes, or a predicate — so one declaration drives the subset validated to advance, what a review screen lists, and which step owns a given path. Advancing validates that subset only; validating the whole model on step one reports errors for fields the user has not been shown, which reads as a broken form. Going back re-renders from the model and never re-seeds from defaults. Because a branch can skip a step, the step list is derived from the model, and the final gate validates the complete active shape.
go deeper
Recall the shape: one state object for the whole flow, and each step shows part of it. Going back should show what was typed, not a blank step.
Explain the two gates — a per-step subset to advance, the whole shape before submit — and why whole-model validation on step one surfaces errors for fields the user has never seen.
Show the invariants you enforce: membership declared as paths including prefixes for repeating groups, flags preserved across back-navigation, later steps cleared only where a real dependency demands it.
Argue the tradeoffs: where the model lives and when it is disposed, how much is persisted for resumability against its privacy cost, and whether advancing is blocking or permissive with per-step completeness.
## One model, several views The first decision is whether each step owns its own state or whether all steps are views over a single model. Almost everything downstream follows from it. | Concern | Model per step | One model, step slices | |---|---|---| | Cross-step rules | need copies or a shared side channel | natural: both paths are in scope | | Going back | restore or re-seed; drift likely | re-render from the model | | Review screen | gather from several sources | read the model | | Payload | merge, with a chance of disagreement | project the model once | | Step isolation | strong, and usually not what you need | weak by design | One model wins for anything beyond two trivial steps. The cost is that "valid" is no longer a single question: the form is *partially* valid, per step, and the design has to say what that means. ## Declare step membership as data Give each step a **declared set of paths** — literal paths, path prefixes, or a predicate — rather than encoding it inside the step's markup. One declaration then serves several readers: - the advance gate, which validates only that subset; - the review or summary screen, which lists a step's fields in order; - a path-to-step lookup, so anything the form learns about a path can send the user to the step that owns it; - progress reporting, which needs to know how much of each step is filled. Prefixes matter because a step often owns a whole repeating group: declaring the group's root covers every row that exists now or later, while a hand-written list of row paths is wrong the moment a row is added. ## What "valid enough to advance" means Advancing runs the rules whose paths fall in the current step's set, and nothing else. Two failures are common: 1. **Validating the whole model to advance.** Required fields two steps ahead report errors immediately, and the user sees a form that objects to fields it has never shown. This is the single most recognisable multi-step bug. 2. **Never validating the whole model.** Each step passed in isolation, so a cross-step rule — a date range spanning two steps, a total that must match a sum entered earlier — is only discovered at submit, or not at all. So there are two gates, and they are different: a **per-step subset gate** to advance, and a **whole-shape gate** before submitting. Cross-step rules belong to the whole-shape gate, and may also run on the later of the two steps once both paths have values. ## Going back, and what must survive Going back is a view change. What must survive is the whole model, plus the interaction flags — a field the user already fixed should not present as untouched, and a step already completed should not lose its verdicts. What must *not* happen: - re-seeding the step from defaults, which silently discards the user's typing; - clearing later steps because "they may now be stale" — unless a genuine dependency says so, in which case clear exactly those paths, not everything; - resetting the step's verdicts so a previously failing field looks clean. If the flow may be resumed after a reload, the surviving unit is the same one: the model plus enough flags to redraw the current position. Deciding what is persisted, and where, is a structural choice made once for the whole flow rather than per step. ## Conditional steps Branches make the **step list itself** dynamic: a choice on step one can remove step three entirely. That means the list of steps is derived from the model, not a constant, and the fields of a skipped step are governed by the same conditional policy as any hidden field — dropped, parked, or deliberately submitted, with their rules made inapplicable rather than merely unsatisfied. The whole-shape gate therefore validates the **active** shape, which is exactly the projection the payload is built from. ## The tradeoffs a lead is choosing - **Granularity of the declaration**: literal paths are precise and rot; prefixes and predicates survive refactors but can over-capture. - **Where the model lives**: inside the flow's component subtree means it dies if the user navigates away; outside means it outlives the flow and needs explicit disposal. - **How much state is persisted**: resumability is a product promise with a storage and privacy cost, and half-filled sensitive fields are the reason to decide it deliberately. - **Strictness of advancing**: blocking on a subset is predictable; letting users skip forward and marking steps incomplete is friendlier and needs a per-step completeness notion. ## Interview shape Say "one model, steps as declared slices of paths", give the two gates, then the back-navigation invariants, then name the tradeoffs you would put to the team. The answer interviewers reward is the one that treats a wizard as a projection problem over a single state model rather than as a sequence of forms.
- Why does validating the whole model on step one look broken to the user?Because it reports errors for fields the user has not been shown. A required field three steps ahead is empty for a legitimate reason, and presenting it as a failure makes the form seem to object to itself. Advancing must validate only the paths declared for the current step.
- Where do rules that compare fields on different steps belong?In the whole-shape gate, and optionally on the later of the two steps once both paths hold values. They are the main reason to keep one model: with a model per step neither side can see the other without copying values forward, and the copies drift from their originals.
- What changes when a branch removes a step entirely?The step list becomes derived from the model rather than fixed, and the skipped step's fields fall under the conditional-field policy: their values dropped, parked or deliberately kept, and their rules made inapplicable. The final gate then validates the active shape, which is also what the payload is projected from.
saying these in an interview costs you the question
- Gives every step its own model and copies values between them
- Runs whole-model validation to leave the first step
- Re-seeds a step from defaults when the user goes back
- Thinks a step's field list only needs to exist in that step
- Assumes passing each step separately means the form is valid
- Treats the step list as constant even when a branch skips one