Skip to content
Chapter 45Lesson 6

Quiz - React Hook Form

Quiz progress

0 / 0

A teammate proposes moving every form in the app to React Hook Form “so they all work the same way,” starting with the login form (email, password, one submit). Which forms actually justify the reach? Select all that apply.

A signup form with a live password-strength meter that fills as the user types.
An invoice editor where the user adds and removes line-item rows.
The login form — email and password, submitted once.
An edit-profile form: name, bio, and an avatar URL, saved in one submit.

You add a live 0/280 character counter beside a registered note field. You read the value with const note = form.watch('note') at the top of the form component. It works, but typing anywhere in the form now feels laggy. What’s the fix?

Move the read into a small child component that calls useWatch({ control, name: 'note' }), so only that child re-renders per keystroke instead of the whole form root.
Wrap the form body in useMemo so React skips re-rendering the untouched fields while note changes.
Switch the note input from register to Controller so RHF stops re-rendering the form on its changes.

Your InvoiceSchema has total: z.coerce.number<number>().positive(). A teammate types the form as useForm<z.infer<typeof InvoiceSchema>>(), ships it, and it type-checks fine. Why is the senior reviewer still asking for useForm<InvoiceInput, unknown, Invoice> instead?

z.infer is the output type, so it mistypes what register and defaultValues track (the input side) — it compiles today only because the gap is invisible until a transform’s input and output diverge, and coerce already makes them diverge.
The single-generic form disables the resolver, so validation silently never runs until the three-parameter form is used.
z.infer is slower at runtime because it re-derives the type on every render; the explicit generics are memoized.

A line-items form renders rows with key={index}. Add works, and removing the last row works — but when the user deletes a middle row while editing a row below it, their half-typed text and cursor jump to the wrong line. What’s going on?

The key is the row’s position, not its identity, so removing a middle row shifts the survivors up and React reuses the old DOM node for a different row. Key by field.id — RHF’s stable per-row render key — and the survivors keep their nodes.
remove(index) mutates the array in place; you need replace with a fresh copy so React sees a new reference and re-renders cleanly.
The rows are missing the line’s domain id from the database, so React falls back to matching by position; add id to each row’s defaultValues.

In your line-items form, a bad amount on row 2 renders fine through that row’s Controller, but the schema’s .min(1, 'Add at least one line') rule never shows when the user empties the list. You’re reading form.formState.errors.lineItems?.message. Where does that array-level error actually live?

form.formState.errors.lineItems?.root — array-level rules report on the field’s root slot, not on .message and not on any row.
form.formState.errors.lineItems?.[0]?.message — the array error attaches to the first row, so reading index 0 surfaces it.
form.formState.errors.root?.lineItems — whole-array errors hang off the form’s top-level root, keyed by field name.

In a three-step invoice wizard, clicking Next on step 1 should validate only step 1’s fields (customer, email) and advance if they pass. Which call does that correctly?

await form.trigger(fieldsForStep(1)), advancing if it returns truetrigger runs the resolver against just the named fields.
form.handleSubmit(goNext) — it validates, then advances on success.
Advance only when form.formState.isValid is true.

A wizard uses conditional rendering ({step === 1 && <CustomerStep />}) and sets shouldUnregister: true on useForm “to keep form state tidy.” A user fills step 1, advances, then clicks Back — step 1’s fields are blank. What happened?

Advancing unmounted CustomerStep, and shouldUnregister: true drops an unmounting field’s value from form state — so Back remounts empty. The default (false) keeps the values; remove the line.
Back calls setStep(step - 1), which re-runs useForm and resets every field to its defaultValues. Guard the reset behind a useRef.
Each step needs its own useForm so its values persist independently; the shared root form can’t hold a hidden step’s data.

Quiz complete

Score by topic