Files
gruperly/.agents/skills/react-hook-form/references/ctrl-usecontroller-isolation.md
2026-09-04 16:49:24 -03:00

2.8 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Isolate Controlled Inputs in Dedicated Child Components HIGH re-renders only the changed field instead of the whole form ctrl, useController, Controller, controlled-components, re-renders

Isolate Controlled Inputs in Dedicated Child Components

Controller and useController are equivalent — Controller is a thin component wrapper around useController. Re-render isolation does not come from picking one over the other. It comes from putting the subscription in a child component, so that when the field value changes, only the child re-renders. Inlining Controller (or useController) in the parent form makes every parent re-render flow through every controlled input.

Incorrect (Controllers inlined in parent — every parent re-render re-renders all controlled inputs):

function PaymentForm() {
  const { control, handleSubmit } = useForm<PaymentFormData>()

  return (
    <form onSubmit={handleSubmit(submitPayment)}>
      <Controller
        name="amount"
        control={control}
        render={({ field }) => <CurrencyInput {...field} />}
      />
      <Controller
        name="currency"
        control={control}
        render={({ field }) => <CurrencySelect {...field} />}
      />
    </form>
  )
}

Correct (subscription moved into dedicated child components, isolating re-renders to the changed field):

function PaymentForm() {
  const { control, handleSubmit } = useForm<PaymentFormData>()

  return (
    <form onSubmit={handleSubmit(submitPayment)}>
      <AmountInput control={control} />
      <CurrencySelectField control={control} />
    </form>
  )
}

function AmountInput({ control }: { control: Control<PaymentFormData> }) {
  const { field } = useController({ name: 'amount', control })
  return <CurrencyInput {...field} />
}

function CurrencySelectField({ control }: { control: Control<PaymentFormData> }) {
  const { field } = useController({ name: 'currency', control })
  return <CurrencySelect {...field} />
}

Equivalent with Controller (also correct — same isolation):

function AmountField({ control }: { control: Control<PaymentFormData> }) {
  return (
    <Controller
      name="amount"
      control={control}
      render={({ field }) => <CurrencyInput {...field} />}
    />
  )
}

When to prefer one API over the other:

  • useController — when you also need fieldState/formState in the same component, or want to compose with custom logic
  • Controller — when you want a single JSX-only declaration and don't need to read state in the surrounding component

Both achieve the same re-render isolation when placed in a child component.

Reference: useController · Controller