Form state is where React applications quietly rot. The first version holds a field in useState, the second adds validation, the fifth adds a nested list of items, and by the tenth the component re-renders the whole page on every keystroke, the validation logic is smeared across effects, and the submit handler defensively re-checks everything because nobody trusts the state anymore. The form libraries exist to stop that decay, and the two that matter for this article solve it with opposite models: Formik built the controlled, render everything approach that matches the React of its era, and React Hook Form built the uncontrolled, register and reference approach that matches the performance requirements of the pages that hurt. The honest comparison, and the per form decision this article ends with, is about the forms the application actually has, because the right answer for a login box and the right answer for a fifty field underwriting wizard are different products.

Where plain useState stops being enough

The plain state version of a two field form is fine, and pretending otherwise is how articles lose credibility. The trouble starts at the second tier, the form with a handful of fields and cross field rules:

const [email, setEmail] = useState('')
const [confirm, setConfirm] = useState('')
const [touched, setTouched] = useState({})
const [errors, setErrors] = useState({})

Each field adds a state, a touched flag, an error string, and the validation effect that runs on all of them, and the re-render fires on every keystroke because the state lives in the component. The third tier is where the architecture collapses: dependent fields, dynamic rows, async validation of unique values, draft persistence, and multi step state that must survive navigation. The component that held two fields in three useState calls now holds a state machine in a dozen, the effects have an ordering nobody can name, and the review conversation about a new field takes longer than writing it. The libraries answer by owning the state machine, the storage, the validation pipeline and the submission flow, and the only real question left is which model of owning suits the forms in question.

The React Hook Form model of registered references

React Hook Form keeps form state outside React's render path. The input registers itself by reference:

const { register, handleSubmit, formState: { errors } } = useForm()

<form onSubmit={handleSubmit(save)}>
  <input {...register('email', {
    required: 'Enter an email',
    pattern: { value: /.+@.+/, message: 'Invalid format' }
  })} />
  <input type="password" {...register('password', {
    required: 'Enter a password'
  })} />
  {errors.email && <p>{errors.email.message}</p>}
</form>

The registration wires the input's change events into the library's internal store, the typing updates that store without re-rendering the component, and the re-render happens only where it is needed, the error message that appears, subscribed through the formState accessor. The performance character is the direct consequence: a fifty field form re-renders the error components, not the field components, and the keystroke latency stays flat as the form grows. The validation integrates with schema libraries through a resolver, Zod or Joi applied once over the whole form, and the schema becomes the single definition of the rules, shared with the server side if the server runs the same schema:

const { register, handleSubmit } = useForm({
  resolver: zodResolver(signupSchema)
})

The costs of the model are the costs of references. Conditional fields need explicit unregister semantics or the values persist past the field's disappearance. Deeply controlled components, rich editors, date pickers that demand value props, need the Controller wrapper, and a form that is mostly controlled components is a form where the uncontrolled advantage is gone and the library's value shrinks to its validation pipeline. The dynamic arrays are first class through useFieldArray, with append, remove and swap operations that manage keys correctly, and the nested object paths, items.0.sku, are part of the register vocabulary rather than a state management problem.

The Formik model of state in the render path

Formik holds the form state in React state and re-renders the form when it changes:

<Formik
  initialValues={{ email: '', password: '' }}
  validate={validateSignup}
  onSubmit={save}
>
  {({ values, handleChange, errors }) => (
    <Form>
      <Field name="email" type="email" />
      <ErrorMessage name="email" component="p" />
      <Field name="password" type="password" />
    </Form>
  )}
</Formik>

The model's strength is visibility and the devtools story: values, errors and touched state are plain React state, the component tree shows them in the React devtools, and the debugging is the debugging the team already knows. The ecosystem integration is mature, the render props and the component abstractions cover the standard cases cleanly, and the validation runs as a plain function or a schema, with the same schema sharing the Hook Form enjoys.

The cost is the same visibility from the other side: every keystroke updates the store, the form re-renders, and on the large forms the re-render is the latency. The library offers performance escapes, the FastField that isolates a field's re-renders, the memoization of heavy subtrees, and each escape is a small piece of the architecture the Hook Form model gives by default. The dynamic arrays work through FieldArray, the async validation works through the validation pipeline, and the multi step forms work through the state that the model already holds. What the model buys is coherence, everything in the React data flow, and what it pays is the re-render tax, which is exactly the trade the comparison must price per form.

The render count comparison that makes the model concrete

The two models' difference is easiest to price with a measurement any team can reproduce. A forty field form, one error message per field, typed into at full speed, instrumented with a render counter per component. The plain useState version re-renders the whole form component tree on every keystroke, forty field components plus their error components, because the state lives in the form component and every change re-runs it. The Formik version re-renders the same tree per keystroke by architecture, the values store updates and the subscribed tree follows, mitigated field by field through FastField until the isolated fields each re-render alone.

The Hook Form version re-renders, per keystroke, one component: the error paragraph of the field being typed into, appearing when a rule fails, and nothing else, because the fields are registered references outside the render path and the subscriptions are per derived value. On the worst forms, the underwriting wizard with conditional sections and a hundred inputs, the difference is the difference between a page that keeps up with typing and a page that visibly trails it on mid range hardware, and the difference compounds with the devtools open, which is exactly when the debugging team is watching. The numbers are not the argument by themselves, the argument is that the render model determines them, and the model is the first thing a team should know about a form library rather than the last.

Testing forms with the two libraries

The form's test surface is the contract, values in, validation verdicts, submission payload out, and both libraries make the contract testable with slightly different ergonomics. React Hook Form tests are naturally close to the user, the test renders the form, types through the inputs, and asserts the submit payload, because the registered inputs are real inputs and the user events are the library's own events. Formik tests run the same user level flow, and both benefit from the schema shared with the server, since the test suite can generate validation cases from the schema itself, the rule that rejects an email without a domain becomes a case list the schema definition produces, and the form under test either enforces the schema or fails the shared contract.

The submission path deserves its own test in either library, the pending state, the disabled button, the error banner on a server rejection, and the draft restoration test for the long forms, load a draft, reset the form, type nothing, assert the values. These tests are the reason the library boundary was worth drawing at all, they run against the form as the user experiences it, and they survive the refactors that the internal validation effects of the plain useState version never did, because there are no internal validation effects to refactor.

The complex cases both libraries must survive

The forms that break libraries are the same everywhere, and the comparison is honest only if it names them. Dependent fields, the country that changes the state list, are plain subscriptions in both, watch in Hook Form, values in Formik, with the render scope priced accordingly. Async validation, the username that must be unique on the server, runs in both validation pipelines with the debounce discipline applied by hand, and the finding both libraries share is that the schema resolver path makes the async rule part of the declared contract rather than an effect bolted beside the form.

Draft persistence is the case that decides library fitness for long forms, the application form that must survive a refresh. Hook Form pairs with state persistence outside React, the values written to a store on change through watch, restored through reset, and the frequency of the writes is a policy decision. Formik holds the values in React state and the persistence subscribes to them, with the same policy decision. The multi step wizard splits into the same pattern in both, a step level form inside a state machine, and the machine, not the form library, owns the navigation and the aggregation. The cases do not crown a winner, they reveal that both libraries solve the form's mechanics and leave the product's architecture, steps, drafts, drafts of drafts, to the same design work either way.

The decision rules per form not per application

The decision that survives production is per form, and the rules that fall out of the two models are mechanical enough to write down:

  1. Default to React Hook Form with a schema resolver for new forms, the performance and the schema sharing are the strongest defaults;
  2. Keep plain useState for genuinely local fields, a search box or a filter toggle is not a form problem;
  3. Wrap third party controlled components in Controller or FastField, and count them, a form of mostly controlled components prices the two models nearly equal;
  4. Hold validation rules in a schema shared with the server, and let the resolver apply it, one definition per rule;
  5. Keep dynamic arrays through the library's array mechanism, and never hand roll key management;
  6. Migrate the forms that hurt, the big and the slow, and leave the stable ones alone, the library boundary is per form already.

The libraries are two answers to the same problem, and the problem is the state machine a form actually is: storage, validation, submission and the error surface, owned by a component that was never designed to be a state machine. The team that gives that machine to a library built for it gets back the component that renders, the schema that defines, and the submit handler that trusts, which is every promise the form libraries ever made, cashed per form where the promises are actually needed.

The per form boundary has one last property worth naming, it is reversible. A form that migrates from one library to the other carries its schema, its test suite and its submission contract across the move, because the shared pieces were always the parts worth owning, and the render model underneath is an implementation detail the user never notices until the form is large enough to make it the whole story.