Capítulo 32 de 39

Chapter 32: useFormState

Core Idea

useFormState({ control, name?, exact? }) subscribes to form state in its own isolated scope, so a component reading e.g. isDirty re-renders on that change without affecting or being affected by other useFormState/useForm subscriptions elsewhere — the standard way to keep large forms' re-renders localized.

Key Concepts

  • control: optional if inside FormProvider, otherwise required.
  • name: single field name, array of names, or omitted to subscribe to all inputs' formState.
  • disabled: turn the subscription off without unmounting the hook.
  • exact: defaults false (partial/prefix name matching) — different default from useController, which defaults exact to true.
  • Proxy-tracked return: like formState everywhere else in RHF, the object is a Proxy that only "activates" a subscription for properties actually read during render — always destructure at the top (const { isDirty } = useFormState()), never keep the whole object and read a property later.
  • isDirty vs dirtyFields: isDirty is a whole-form flag (can flip from field-array insert/remove without any single field being in dirtyFields); dirtyFields is per-field, and both require defaultValues to be set for correct comparison.

Code Examples

function Child({ control }) {
  const { dirtyFields } = useFormState({ control })
  return dirtyFields.firstName ? <p>Field is dirty.</p> : null
}
  • What it demonstrates: an isolated subscription — Child re-renders only when dirtyFields changes, not on every keystroke elsewhere in the form.

Reference Tables

formState fieldMeaning
isDirtytrue once any input differs from its defaultValues
dirtyFieldsper-field modified map
touchedFieldsper-field interacted-with map
isSubmitted / isSubmitSuccessful / submitCountsubmission history
isSubmittingtrue while a submit is in flight
isLoadingtrue while async defaultValues are loading
isValidtrue once validation has run at least once and found no errors — stays stale without a resolver or a mode other than onSubmit until trigger()/submit runs
isValidating / validatingFieldsin-flight validation tracking
errorsfield error object
disabledmirrors useForm's disabled option
isReadytrue once the formState subscription itself is set up

Anti-patterns

  • const formState = useFormState() without destructuring: defeats the Proxy's per-field subscription tracking — always destructure the specific keys you need.
  • Trusting isDirty alone to mean "this specific field changed": it's a whole-form signal; check dirtyFields.<name> for a per-field answer.

Key Takeaways

  1. Destructure useFormState's return immediately — reading a property later defeats its Proxy-based subscription.
  2. Scope subscriptions with name to avoid re-rendering on unrelated field changes in large forms.
  3. isValid needs at least one validation pass (resolver, or a mode beyond default onSubmit, or an explicit trigger()) before it's meaningful.

Connects To

  • useform-formstate: the same state shape, read via useForm() directly instead of the isolated hook.
  • useformstate-errormessage: companion component for rendering error text from this state.
  • usecontroller: also returns a scoped formState slice alongside field/fieldState.