In a form with a repeating 'Add another' group, what does the form state hold for those rows?
answer
- a list, not a fixed field set
- values are not the only per-row state
- errors and flags live per row too
- remove the row and its side entries
- identity is minted, position is not
basics
~20 sAn array of row objects, one entry per repeated group, plus per-row errors and touched flags addressed by the same row. Adding appends a row; removing must delete that row and every entry addressed under it.
solid answer
~50 sA repeating group is a list, not a fixed set of fields, so the model holds an **array** whose entries are objects carrying that group's fields. Three more things hang off each row: its **errors**, its **interaction flags** (touched and dirty, so a brand-new row does not show errors before it is edited), and an **identity** that names the row itself rather than the slot it currently occupies. Adding appends a row seeded from that group's defaults, so every field starts at a defined value. Removing has to delete the row's values together with its error and flag entries — many models leave those behind, and the leftovers then describe the wrong row. Reordering moves the row and everything attached to it; if the errors are keyed by position instead of by the row, they stay put and now belong to a neighbour.
go deeper
Recall the shape: an array of row objects, plus errors and touched flags per row, plus an id for each row. Add appends from defaults; remove deletes the row and everything stored under it.
Explain why the side tables are the bug farm: values move on reorder and delete, and any table keyed by position does not move with them. Walk through add, remove and reorder and name what each has to touch.
Show that you have debugged the leftovers — an invisible error from a deleted row blocking submit, a touched flag inherited by the row that took the slot. Make removal one atomic update over every table.
Frame it as a data-model decision the whole form language inherits: whether rows carry identity, whether editing state is addressed the same way as values, and whether the payload is derived from the model or is the model.
## What a repeating group is A repeating group is a set of fields the user can have **zero, one, or many of**: line items on an invoice, phone numbers on a contact, passengers on a booking. Its shape is not known when the form is written — it changes while the user fills it. That makes it a **state-modelling** problem wearing a form's clothes: the interesting part is not the markup that draws a row, it is what the model stores per row and what each operation has to touch. ## The four things the model holds Only the first is obvious, and the other three are where the bugs live: - **Values** — an array whose entries are objects, one per row, each holding that group's own fields. - **Errors** — a verdict per field *per row*, reachable from the row, not from a single flat list. - **Interaction flags** — touched and dirty per field per row, so a freshly added row renders blank instead of instantly red. - **Identity** — a handle minted when the row is created that names *that row*, independent of where it currently sits in the array. A fifth exists in models that track which controls are currently live: the **registration** of the row's fields, so rules run for fields that exist and stop running for fields that do not. ## The three operations 1. **Add** — append (or insert) a row built from the group's defaults, so every field has a defined starting value rather than an absent one. Do not mark the new row touched; it has not been edited. 2. **Remove** — delete the row's values *and* the error and flag entries addressed under it, in the same update. Removing only the value is the most common defect in dynamic forms: the row disappears and its old error keeps being reported. 3. **Reorder** — move the row's entry in the array. Anything keyed by the row's identity travels with it for free; anything keyed by position does not move at all, and now describes whichever row slid into that slot. | Operation | Values array | Errors and flags | Classic bug | |---|---|---|---| | Add | new entry from defaults | no entries yet | new row shows errors immediately | | Remove | entry deleted | entries under that row deleted too | stale error blocks submit invisibly | | Reorder | entry moves | moves only if keyed by row identity | error and value belong to different rows | ## Why the side tables are the bug farm The values array is the only table that is *automatically* correct after a list operation, because the path to a value is wherever it now sits. Everything else is stored under a key someone chose: - an error entry left behind by a deleted row keeps the form invalid with no control to blame; - a touched flag left behind makes the row that took the slot render red before it is edited; - a dirty flag left behind makes an untouched form claim it has unsaved changes; - a registration left behind keeps a rule running for a field that no longer exists. ## Position is not identity A row's position is the one property that changes for reasons that have nothing to do with the row: inserting above it, deleting above it, sorting. So a model that addresses a row by its position is addressing something that moves under it. Giving each row an id when it is created — a counter, a generated token, or a natural key the data already carries — gives every side table something stable to hang on. How a *rendered* list is matched to the instances drawn last time is a related but separate question about rendering identity; this one is about what the model stores. ## What the payload is not The submitted payload is usually just the values array, sometimes reshaped. Errors, flags and row ids are **editing state**: they exist to run the screen and are stripped on the way out. Candidates who think only about the payload model the array and nothing else, then cannot explain why a deleted row still blocks submission. ## Where frameworks differ The array-of-objects model is common to all of them; what differs is how an update is expressed and what a removal implies. A runtime that re-runs the component function on every state write expects a **new array** rather than an edit in place, and a stale derived value is the usual failure. A fine-grained runtime tracks each row's own state and only the removed row's subtree tears down, so removal is cheaper but a leftover error entry is just as possible. A compile-time runtime may let you mutate the list and still notice, which makes accidental partial updates easier to write. In every case the rule is the same: **one update moves the row and everything addressed under it**. ## What a good answer sounds like Say "an array of row objects, plus per-row errors and flags, plus a row identity," then name the operations and what each has to touch. Mention that a removal is a multi-table delete and that a reorder is free only if the side tables are keyed by the row rather than the slot. That is the whole answer at this level.
- Why seed a new row from a defaults object instead of leaving its fields absent?An absent field and an empty field behave differently: rules may skip an absent value, a control bound to it can flip between owned and unowned, and the payload shape becomes inconsistent between rows. Seeding from defaults makes every row the same shape from the first render, so rules, controls and the payload all see one contract.
- What breaks if the remove handler deletes a row's value but leaves its error entry?The form stays invalid because of a field nobody can see or fix. Submit is blocked, the error summary points at a row that no longer exists, and the user's only escape is a reload. It is the reason a removal must be one update over values, errors and flags together.
saying these in an interview costs you the question
- Thinks a repeating group is just more fields in flat state
- Removes a row's value but leaves its error and flag entries
- Assumes reordering rows needs no change to per-row state
- Keys every row by its array index and calls that stable
- Marks a freshly added row touched, so it shows errors at once
- Believes only the submitted payload shape matters, not editing state