Capítulo 2 de 39

Chapter 2: Advanced Usage

Core Idea

This page is a recipe collection for problems that come up once a form grows past the basics: accessibility, multi-step wizards, composing reusable input components, deep component trees, performance, testing, and integrating with Next.js Server Actions / useActionState.

Key Concepts

  • aria-invalid + role="alert": pairing the field's aria-invalid attribute with a role="alert" error span is what makes RHF's validation errors actually announced by screen readers.
  • FormProvider / useFormContext: puts the useForm() return value on React Context so deeply nested inputs can call register/control without prop drilling.
  • ConnectForm pattern: a small wrapper combining useFormContext with a render-prop child, used to connect one input deep in a tree without re-plumbing context everywhere.
  • Field/Input transform: wrapping Controller's field.onChange/value to convert between the DOM's string values and a richer type (number, Date, etc.), while still spreading the rest of field so ref/onBlur/name survive.
  • useActionState + handleSubmit: run RHF validation inside handleSubmit, then hand the already-parsed data to the Server Action's dispatch function — no useEffect bridge needed.

Code Examples

// Accessible error pattern
<input
  id="name"
  aria-invalid={errors.name ? "true" : "false"}
  {...register("name", { required: true, maxLength: 30 })}
/>
{errors.name?.type === "required" && <span role="alert">This is required</span>}
  • What it demonstrates: the two attributes (aria-invalid, role="alert") that turn a plain validation error into one a screen reader announces.
// Server Action + useActionState, no useEffect bridge
const [state, submitAction, isPending] = useActionState(updateProfile, null)
const { register, handleSubmit, formState: { errors } } = useForm()

<form onSubmit={handleSubmit((data) => submitAction(data))}>
  • What it demonstrates: RHF validates client-side first; only a valid submission reaches the Server Action, and isPending/state come straight from useActionState with nothing to synchronize manually. Field-level server errors get mapped back with setError inside a useEffect that watches state — a legitimate use of the effect since it syncs external state in.

Anti-patterns

  • Wrapping render() in act() during tests: RHF's internal async validation already causes a post-test-return render; wrapping in act() unnecessarily hides real problems — use find* queries instead and await them.
  • Overriding onChange/value on a Controller field without spreading the rest of field: drops ref (breaks focus-on-error) and onBlur (breaks isTouched).
  • Rendering thousands of inputs without virtualization: naive virtualized lists reset unmounted rows to default values on re-entry; register through FormProvider/useFormContext per row, or use Controller with an explicit defaultValue sourced from getValues(), to survive mount/unmount cycles.

Key Takeaways

  1. Accessibility in RHF forms is opt-in — aria-invalid and role="alert" have to be added by hand.
  2. FormProvider/useFormContext is the standard way to avoid prop-drilling register/control through deep trees, at the cost of the whole subtree re-rendering on form updates (mitigate with memo + a custom prop comparator).
  3. For virtualized lists, plan for items to unmount and remount — read defaults from getValues(), don't rely on component state surviving.
  4. With React's useActionState, RHF validation runs first inside handleSubmit; the Server Action only ever receives already-valid data.

Connects To

  • useFormContext: the hook FormProvider exposes to descendants.
  • useController / Controller: needed for the transform-value and virtualized-list patterns.
  • useForm/seterror: how server-returned field errors get mapped back onto the form.
  • useForm/form: the <Form> component's action prop is the lower-level alternative when you don't need useActionState's pending/result state.