skip to content

Why does typing into a React Hook Form input usually not re-render the form component, when the same keystroke in a Formik form does?

level: middleimportance: must knowfreq 62%

answer

  1. where the value lives
  2. DOM plus internal store
  3. formState is a Proxy
  4. Formik: state in the form owner

basics

~20 s

React Hook Form leaves registered values in the DOM and its own store, and re-renders only for formState you read. Formik keeps values in the form owner's state, so each keystroke re-renders that component and its children.

solid answer

~40 s

A registered input is **uncontrolled**: on each change RHF writes the new value into its internal store and only notifies subscribers whose state actually changed. The component that called `useForm` subscribes through `formState`, which is a **Proxy** — it re-renders only for the properties it read during render (`errors`, `isDirty`, `isValid`...). If nothing it reads changes, a keystroke costs no React render at all. **Formik 2**, by contrast, holds `values`, `errors` and `touched` in the component that runs `useFormik` or `<Formik>`; `handleChange` updates that state, so the owner and the fields rendered below it re-render per keystroke unless you opt into Formik's `<FastField>`. RHF can still re-render a lot — `watch()` at the root, reading many `formState` flags, or `mode: 'onChange'` producing new errors — but the default costs nothing.

code

tsx · 24 lines
tsx
import { useForm, useFormState, type Control } from 'react-hook-form';

type Profile = { displayName: string; bio: string };

function SaveButton({ control }: { control: Control<Profile> }) {
  // Isolated subscription: only this button re-renders when these flags change.
  const { isDirty, isValid } = useFormState({ control });
  return <button disabled={!isDirty || !isValid}>Save</button>;
}

export function ProfileForm({ onSave }: { onSave: (p: Profile) => void }) {
  const { register, handleSubmit, control } = useForm<Profile>({
    defaultValues: { displayName: '', bio: '' },
    mode: 'onTouched',
  });
  // This component reads no formState, so typing does not re-render it.
  return (
    <form onSubmit={handleSubmit(onSave)}>
      <input {...register('displayName', { required: true })} />
      <textarea {...register('bio')} />
      <SaveButton control={control} />
    </form>
  );
}

go deeper

for a junior

Remember the one-line version: RHF keeps values out of React state; Formik keeps them in the form owner's state.

for a middle

Explain the formState Proxy, the short-circuit trap, and the explicit ways a component opts into re-renders.

for a senior

Use the profiler to find which subscription re-renders a slow form, then move it into useFormState or useWatch children.

for a principal

Weigh a controlled model's simplicity against uncontrolled performance when setting one form library for many teams.

## The mechanism in React Hook Form React Hook Form (RHF) registers **uncontrolled** inputs. `register('email')` gives the input a `ref` and native `onChange`/`onBlur` handlers, but no `value` prop, so React never has to re-render to show the character the user typed: the browser already drew it. On each change event RHF: 1. Reads the new value from the event target. 2. Writes it into its internal form-values store. 3. Decides whether this event should validate, based on `mode` and `reValidateMode`. 4. Updates dirty/touched/error state and **notifies only subscribers of what changed**. The last step is the whole performance story. Components subscribe to RHF state in a few explicit ways, and a keystroke re-renders a component only when something it subscribed to moved. ## formState is a Proxy: read what you use The `formState` object returned by `useForm` is wrapped in a JavaScript **Proxy**. While the component renders, the Proxy records which properties it read — `errors`, `isDirty`, `dirtyFields`, `touchedFields`, `isValid`, `isSubmitting` and so on — and RHF later triggers a re-render only when one of **those** properties changes. Two consequences follow: - A form that reads only `errors` and `isSubmitting` does not re-render when the user makes the form dirty. - Reading must happen **during render**. The documented trap is a short-circuit: `disabled={!formState.isDirty || !formState.isValid}` never reads `isValid` while `isDirty` is `false`, so the Proxy may not subscribe to it. Destructure both first: `const { isDirty, isValid } = formState;`. If a child needs form flags without re-rendering the parent, `useFormState({ control })` gives it its own isolated subscription. ## Where Formik differs **Formik 2.x** (the rival this question compares against) takes the opposite route: the component that calls `useFormik`, or the `<Formik>` component, owns `values`, `errors` and `touched`, and its `<Field>` inputs are **controlled** from those values. `handleChange` updates the owner's state, which re-renders the owner and, through context, the fields beneath it. | | React Hook Form 7 | Formik 2 | |---|---|---| | Where a typed value lives | DOM input plus RHF's store | State of the component owning the form | | Input style | Uncontrolled via `register` | Controlled via `<Field>` / `field` props | | Re-render on a keystroke | Only subscribers whose state changed | The owning component and its field subtree | | Opt-in to fewer renders | Default behaviour | `<FastField>` for independent fields | | Opt-in to more renders | `watch()`, `formState` reads, `Controller` | Default behaviour | Neither is wrong. A controlled model makes every value available to render logic at every moment; the uncontrolled model makes large forms cheap by default and asks you to subscribe deliberately. ## What makes React Hook Form re-render anyway - **`watch()` in the `useForm` component** subscribes that component to value changes, so it re-renders on every keystroke of the watched fields (all fields if called without arguments). - **Reading many `formState` flags** at the root, such as `isDirty` or `dirtyFields`, subscribes the root to them. - **`mode: 'onChange'`** validates on every change; when errors appear or clear, `errors` subscribers re-render. - **`Controller` fields** are controlled, so they re-render themselves on each change — but only themselves, not the whole form. ## Judging it in an interview The strongest answer names the mechanism, not a slogan: *values are not React state in RHF, and subscriptions are opt-in and scoped.* A candidate who says RHF "never re-renders" is overstating it, and one who says Formik "is slow" is missing that a small Formik form re-renders cheaply and that the cost only shows as fields multiply.

  • A React Hook Form component renders disabled={!formState.isDirty || !formState.isValid} and the button stays disabled after the form becomes valid. Why?
    `formState` is a Proxy that subscribes only to properties read during render. While `isDirty` is `false`, the `||` short-circuits and `isValid` is never read, so the component may not be subscribed to it. Destructure first — `const { isDirty, isValid } = formState` — so both are read on every render.
  • How do you let one child show form flags without re-rendering the whole React Hook Form form?
    Pass `control` to the child and call `useFormState({ control })` there. It has its own subscription scope, so changes to `isDirty` or `errors` re-render only that child. `useWatch` does the same for values, which keeps `watch()` out of the root.
  • Does React Hook Form's approach make a Controller-wrapped field as cheap as a registered one?
    No. A `Controller` renders a controlled component, so that component re-renders on every change to show the new value. The cost is scoped to the Controller, not the whole form, which is why mixing a few controlled widgets into a large registered form stays cheap.

React Hook Form is a teacher who lets students write on their own sheets and only walks over when a student raises a hand for something the teacher asked to hear about; Formik copies every word onto the classroom board, so the whole class looks up each time.

saying these in an interview costs you the question

  • React Hook Form never re-renders anything while the user types
  • formState is a plain object snapshot, so reading it has no side effects
  • React Hook Form is faster because it debounces setState on each input
  • Formik re-renders less because it batches keystrokes until blur
  • Calling watch() at the form root is free because it only reads values