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 needfieldState/formStatein the same component, or want to compose with custom logicController— 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