Articles
React Hook Form Interview Questions: Field Arrays, Default Values, and Reset Behavior
Practice React Hook Form field arrays with stable row keys, cached and async defaults, keepDirtyValues, reset baselines, and a concrete order-form test matrix.
An editable order form loads three line items from a server. The user edits the second quantity, removes the first item, and then receives fresher server data. Which values survive, which rows remount, and what does Reset mean now? That is the real React Hook Form interview challenge. A strong answer separates row identity, registered field paths, and the default-value baseline before choosing an array operation or reset option.
The library behavior below comes from the current React Hook Form useFieldArray documentation, useForm options, and reset reference. The order form and test cases are original practice, not a reported employer prompt. The library's official GitHub documentation source was also checked because its website sometimes blocks automated reading.
What are the three kinds of identity in a field array?
A line item may have a database ID such as sku-42, a React key used to preserve the rendered row, and a registered field path such as items.1.quantity. Those are different things. The backend ID identifies a domain record across saves. useFieldArray generates field.id to use as the React key. The numeric index is the row's current position in the form array, so it can change after removal or reordering.
The official useFieldArray rules explicitly say to key mapped rows by field.id, not by index. If row A is removed, B moves from index 1 to index 0. Keying by index can cause React to reuse a row component for different data, with stale local UI state or input behavior. The registered path must still use the current index: register(\items.${index}.quantity`)`. “Use the ID everywhere” is as wrong as “use the index everywhere.”
If server records also contain a property named id, check the library's keyName behavior and the version in the exercise. Current documentation warns that the generated identifier can overwrite a field object's id under the default key name and says keyName is slated for removal in a future major version. A robust domain model keeps the server identifier under a separate name such as itemId, rather than silently treating field.id as the server's key.
How would you build a minimal editable order form?
Start with a complete shape for every row: { itemId, description, quantity }. The useForm defaults should include items: [] even before remote data arrives. Then useFieldArray({ control, name: 'items' }) exposes fields and operations such as append, remove, move, update, and replace. A small implementation sketch makes the identity choice visible:
const form = useForm({
defaultValues: { items: [] },
});
const { fields, append, remove, move } = useFieldArray({
control: form.control,
name: 'items',
});
return fields.map((field, index) => (
<div key={field.id}>
<input {...form.register(`items.${index}.description`)} />
<input type="number" {...form.register(`items.${index}.quantity`, {
valueAsNumber: true,
})} />
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
));
The snippet is intentionally incomplete: a production form still needs labels, errors, minimum-row rules, submit handling, and focus decisions. Its purpose is to show field.id as React identity and index as the registration path. append should receive a complete row object; the official API says append data cannot be partial. Choose an explicit blank row rather than relying on undefined values to become sensible defaults.
The array supports rows that are objects; the documentation does not treat an array of primitive strings as the intended useFieldArray shape. If the product wants a list of tags, wrap each in an object or choose a different state model. That detail often surfaces when a candidate proposes append('new') and later wonders why registration or validation is awkward.
What happens when a row is removed or moved?
In our fixture, the initial order is A, B, C and the user edits B's quantity. After remove(0), the visible order is B, C; B's registered path changes from items.1.quantity to items.0.quantity. The React key should remain B's generated field ID for that operation, so its row component is not confused with A. The submitted array should contain B and C, in that order, with the edited quantity associated with B.
For move(1, 0), the rows swap position; again, the registration index changes, but each rendered row should follow its stable generated key. Do not cache a captured index in a long-lived handler that later acts on a different row after reordering. The current render's index is the relevant path. If a row component has local UI state such as whether its details are expanded, decide whether that state should follow the row's identity or reset with its position.
The official documentation distinguishes update from targeted setValue: an update(index, object) operation unmounts and remounts the updated row. That can clear local row state or focus. If only one input value needs changing and preserving the mounted row matters, setValue may be more appropriate. replace swaps the entire field array's values; it is useful for a genuinely new dataset but can be a poor response to a tiny server patch. Explain the desired behavior before choosing the API.
Why don't new defaultValues props automatically update the form?
The useForm reference says defaultValues populate the form and are cached. Changing the object passed as defaultValues on a later parent render is not the same as updating the form's values. The library supports an asynchronous defaultValues function, a reactive values prop, and an explicit reset(newValues) call; each expresses a different relationship between server state and edits.
Suppose the form first renders empty, then GET /order/42 returns three items. One approach is to use the documented async defaultValues function. Another is to wait for the server record and call reset(record) at the appropriate point, or use values with carefully chosen resetOptions. Merely assigning a new defaultValues object each render is not a controlled refresh strategy. Also avoid undefined defaults for inputs that will be controlled through Controller; the docs warn about that conflict.
There is a timing question: what if the user starts typing before the remote record arrives? A blind reset to the server payload can wipe those edits. The correct policy depends on whether an early edit was allowed, whether the fetched record is newer, and whether merging is safe. State the product decision: block editing until the first load, preserve touched fields, or show a conflict. keepDirtyValues is a library option, not a substitute for defining that policy.
What does reset do, and which options matter?
Calling reset(newValues) can update field values and the baseline against which future dirtiness is measured. The official reset options distinguish keepDirty, keepDirtyValues, keepValues, and keepDefaultValues. These names are easy to conflate. keepDirty preserves dirty state temporarily but does not preserve the actual input values. keepDirtyValues keeps dirty field values while updating non-dirty fields from the new values; the documentation notes that dirtyFields must be subscribed. keepValues leaves current input values unchanged. keepDefaultValues retains the original baseline and recalculates dirtiness against it when new values are supplied.
For a successful save, a common intentional action is reset(savedRecord) when the server response is now the accepted baseline. For a background refresh while a user is editing, reset(freshRecord, { keepDirtyValues: true }) may preserve dirty values and update clean ones; test nested arrays carefully because row additions, removals, and server changes can make field-path comparisons ambiguous. The product may need an explicit conflict UI rather than an automatic merge of two structurally different item lists.
When values or asynchronous defaults update, the useForm reference says the reset API is invoked internally and resetOptions configures the behavior. That matters if a parent passes a freshly created values object on each fetch: repeated reactive updates can surprise the user. Do not call reset during rendering or before the form's subscriptions are ready; follow the library's reset guidance and make the trigger a deliberate lifecycle or user action.
What test matrix would prove the behavior?
A useful interview test does not stop at “the form submits.” It checks visible inputs, submitted data, row identity, dirtiness, and focus after each operation. Use B's quantity as the sentinel edited value and A's removal as the structural change. Test a remote refresh that changes C's description while B remains dirty. Then test save-and-reset with a server response that reorders rows.
| Action | Expected result | Mistake exposed |
|---|---|---|
| Load order A, B, C | Three complete rows and a clear initial baseline | Treating late props as automatic defaults |
| Edit B, remove A | B and C remain; B's edit follows B | Index used as React key |
| Move C before B | Domain values stay with their rows | Stale captured index or row-local state confusion |
| Refresh a clean field | New server value appears under chosen policy | Unintended blanket reset |
| Refresh while B is dirty | B survives only if preservation was deliberately configured | Assuming keepDirty means keep input value |
| Reset after successful save | Visible values and dirty baseline reflect saved response | Resetting UI but retaining stale defaults |
This matrix is an original exercise, not a claim that every library version handles every nested-array conflict identically. Run it against the exact installed version, especially where keepDirtyValues and array structure interact. In a live interview, narrate the expected product outcome first, then demonstrate which API choice produces it.
How do controlled inputs and validation alter the answer?
A custom select or date picker controlled by a third-party component often needs Controller or useController. Give it a defined default and pass value, onChange, and onBlur according to the component contract. The official Controller docs warn against combining shouldUnregister carelessly with field arrays because reorder and remount can unregister fields. That is a targeted caveat, not a blanket ban on controlled components.
Validation errors follow registered paths such as items.0.quantity. After moving or removing rows, verify that the error appears on the correct visible item. A minimum-item rule and row-level required fields should be tested separately. On append, decide where focus goes and whether validation runs immediately or on submit. An accessible form labels every input with enough row context so “Quantity” is distinguishable across repeated rows.
A concise interview answer
“I would give each item a domain ID, use the generated field.id as the React row key, and use the current index only in register paths. I would load complete defaults through a documented async or reset path, not assume new props rewrite the cached baseline. Before applying remote updates, I would define whether dirty edits survive; keepDirty preserves dirty metadata, while keepDirtyValues preserves the dirty inputs. After a successful save, I would reset to the saved record if it becomes the new baseline. My tests remove and move rows after editing B, then refresh and reset, checking submitted item IDs, values, dirtiness, errors, and focus.”
That answer distinguishes value, identity, and baseline. It also keeps the library contract separate from a product-specific merge policy. The latter is an inference and design choice that an interviewer should ask you to justify, not an official React Hook Form guarantee.
Sources and further reading
- React Hook Form:
useFieldArray— generated keys, object rows, array operations, and remount behavior. - React Hook Form:
useForm— cached/async defaults, reactive values, and reset options. - React Hook Form:
reset— differences between preserving dirty state, dirty values, all values, and defaults. - React Hook Form:
Controller— controlled component wiring and unregister caveat. - React Hook Form official documentation source — readable source for the field-array reference.