Back to React
React
medium
mid

How would you manage form validation, error handling, and state for dynamic inputs?

Use a form library (React Hook Form) for state, validation, and field arrays rather than hand-rolling. Validate with a schema (Zod/Yup). Track per-field touched/dirty/error; validate on blur + submit. For dynamic inputs use field arrays with stable ids. Always re-validate on the server.

5 min read·~10 min to think through

Forms with dynamic inputs hit three hard problems at once — state, validation, dynamic field arrays — and hand-rolling all three is where bugs live.

Use a form library

For anything non-trivial, reach for React Hook Form (or Formik). It gives you:

  • Per-field state without re-rendering the whole form on every keystroke (uncontrolled + refs under the hood).
  • touched, dirty, isValid, isSubmitting, per-field error.
  • useFieldArray — purpose-built for dynamic add/remove inputs.

Validation — use a schema

Define rules declaratively with Zod or Yup, plugged into the form via a resolver:

js
const schema = z.object({
  email: z.string().email(),
  items: z.array(z.object({ name: z.string().min(1), qty: z.number().positive() })).min(1),
});

One schema is the single source of truth — and the same schema can validate on the server. Schema validation beats scattered inline if checks.

When to validate (UX matters)

  • On blur for the first validation of a field — don't scream at the user mid-typing.
  • On change after a field has errored once — so they see it clearing as they fix it.
  • On submit — validate everything, focus the first invalid field.
  • Disable submit while isSubmitting; show a spinner.

Dynamic inputs — field arrays

For "add another item" rows, use useFieldArray. The critical detail: stable keys. The library gives each row a generated id — use that as the React key, never the array index, or removing a middle row corrupts which input shows which value/error.

jsx
const { fields, append, remove } = useFieldArray({ name: "items", control });
fields.map((field, i) => <Row key={field.id} index={i} />); // field.id, not i

Error handling

  • Field-level errors inline next to the input, with aria-describedby / aria-invalid for accessibility.
  • Form-level / submit errors (server rejected, network failure) shown at the top, and map server validation errors back onto the right fields (setError("email", ...)).
  • Keep the user's input on failure — never wipe the form.

The non-negotiable

Client validation is UX; the server must re-validate. Client checks are bypassable — the same schema runs server-side as the real gate.

The framing

"I don't hand-roll it — React Hook Form for state and useFieldArray for the dynamic rows, plus a Zod schema as the single source of validation truth that also runs on the server. Validate on blur first, then on change once a field has errored, and everything on submit with focus to the first error. The dynamic-input trap is keys: use the library's stable field id, never the index, or removing a row scrambles values and errors. Server errors get mapped back onto their fields, and the user's input is never lost on failure."

Follow-up questions

  • Why use the field array's id instead of the index as the key?
  • When should validation fire — on change, blur, or submit?
  • How do you map server-side validation errors back to fields?
  • Why re-validate on the server if the client already validated?

Common mistakes

  • Using the array index as the key for dynamic rows — corrupts state on remove.
  • Validating aggressively on every keystroke from the start — hostile UX.
  • Scattered inline if-checks instead of one schema.
  • Treating client validation as sufficient — skipping server validation.
  • Wiping the form on a failed submit.

Performance considerations

  • Controlled forms re-render on every keystroke; React Hook Form's uncontrolled/ref approach isolates re-renders to the changed field — important for large or dynamic forms. Debounce async validators to avoid request spam.

Edge cases

  • Removing a middle row from a dynamic field array.
  • Cross-field validation (confirm password, end-date after start-date).
  • Async validation (username availability) with debounce.
  • Server rejects with field-specific errors after client passed.

Real-world examples

  • An invoice form with a dynamic list of line items via useFieldArray.
  • Signup forms using a Zod schema shared between client and API.

Senior engineer discussion

Seniors default to a form library + schema validation, design the validation timing for good UX, nail the stable-key requirement for field arrays, map server errors back to fields, and insist client validation never replaces server validation.

Related questions