When to use: a form's inputs are spread across deeply nested components.
How: wrap the form in <FormProvider {...methods}>; descendants call useFormContext() to get register/control/handleSubmit without any prop passed to them.
Trade-offs: the whole provider subtree can re-render on form updates unless consumers scope their own subscriptions with useFormState/useWatch. Never nest two FormProviders — context doesn't merge.
When to use: a large form where a value/state read in one small area shouldn't re-render the whole form.
How: extract a small child component that calls useWatch({ control, name }) or useFormState({ control, name }) instead of reading watch()/formState at the root.
Trade-offs: more components to wire up control through, but re-renders stay scoped to where the value is actually used.
When to use: integrating a UI library input (MUI, react-select, AntD, date pickers) that doesn't forward a native ref/value/onChange contract RHF understands.
How: wrap it in <Controller render={({ field }) => <Lib {...field} />} /> or call useController directly for a reusable wrapped component.
Trade-offs: adds a layer of indirection vs. register; never register the same field both ways.
When to use: validation rules are complex, shared with a backend, or better expressed as a schema (Zod, Yup, Joi, etc.) than inline register rules.
How: useForm({ resolver: zodResolver(schema) }) (or the equivalent adapter); resolver and built-in rules/validate are mutually exclusive per field.
Trade-offs: schema errors must be hierarchical (nested), not dot-notation flat keys, when hand-writing a custom resolver.
When to use: a variable-length list of grouped inputs (line items, contacts, tags).
How: useFieldArray({ control, name }), key each row by field.id (never index), mutate via append/remove/insert/move/swap/update/replace.
Trade-offs: entries must be objects (no flat primitive arrays), shouldUnregister: true is unsupported, and mutation payloads must be complete (no partial/empty objects).
When to use: one field's value should be computed from others (a total, a combined name, a conditional default).
How: useWatch the source fields, then setValue the derived field inside a useEffect gated on the sources actually having values (and often touchedFields, to avoid firing before the user has interacted).
Trade-offs: watch/setValue chains can cascade if not carefully scoped; prefer computing in render (useWatch + plain JS) over setValue when the derived value doesn't need to be a real form field.
When to use: an async onSubmit call can fail server-side after client validation already passed.
How: wrap the request in try/catch; on failure call setError("root.serverError", { type, message }) (or setError(fieldName, ...) for field-specific server errors); read errors.root.serverError in JSX.
Trade-offs: root.* errors don't persist across submissions the way field errors do — re-set them on each failed attempt.
When to use: any input that can unmount (conditional rendering, modal/tab forms, wizard steps).
How: either accept RHF's default (shouldUnregister: false — values retained on unmount) and build a separate form per step/modal, or explicitly call unregister alongside actually removing the element from the tree.
Trade-offs: shouldUnregister: true is a global useForm setting, not overridable per input, and is unsupported together with useFieldArray.