Form State Libraries Compared: RHF, Formik, TanStack Form
AI generated
</>
{ }
React · React Hook Form · Formik · TanStack Form
Form State Libraries
React Hook Form, Formik and TanStack Form compared

Managing a twenty field form with its own validation state per field through useState quickly leads to dozens of rerenders per keystroke. Form state libraries encapsulate this complexity, but differ fundamentally in architecture, performance and integration with schema validation.

17 min read Controlled · Uncontrolled · Zod · Server Actions React 19 · React Hook Form 7 · Formik 2

1. Why form state is a problem of its own

Form state differs fundamentally from ordinary application state: a form with twenty fields does not just have twenty values, it also has a validation status, an error message, a touched status and often a dirty status per field, indicating whether the value has changed since the last save. If all of this state is managed naively with useState per field, every keystroke in one field theoretically changes the entire component tree, which causes noticeable performance problems on larger forms.

Specialized form state libraries solve this problem through different architectural approaches: some keep form state entirely outside the React rerender cycle, others isolate rerenders to individual fields through targeted subscription mechanisms. Choosing the right library depends heavily on how large forms typically are in an application and how much validation needs to happen in real time while typing.

All three libraries covered in this article, React Hook Form, Formik and TanStack Form, solve the same core problem but differ significantly in performance characteristics, bundle size and integration effort with schema validation libraries like Zod.

2. Controlled versus uncontrolled: the architectural difference

In a controlled component, React keeps the current value of an input field in state, every keystroke triggers an onChange event that updates the state and thereby triggers a rerender of the component. In an uncontrolled component, the browser itself manages the input field's value in the DOM, React only reads the value when needed, for example through a ref, without triggering a rerender on every keystroke.

This architectural difference is the main reason for the differing performance characteristics of the three libraries. React Hook Form consistently relies on uncontrolled components and refs, so most keystrokes are processed entirely in the native DOM without a React rerender. Formik, on the other hand, is built on controlled components with explicit state per field, which stays closer to the classic React model but produces more rerenders on large forms.

3. React Hook Form in detail: uncontrolled, performance first

React Hook Form registers fields through the register function, which returns a ref plus event handlers that get bound directly to a native input element. Because React Hook Form tracks field values internally through refs rather than React state, typing in a field by default causes no rerender of the surrounding component, which brings a noticeable performance advantage over controlled approaches on forms with many fields.

The useForm hook returns, besides register, also handleSubmit, which automatically runs validation before calling the actual submit handler, as well as formState with fields such as errors and isSubmitting. Important for performance: access to formState fields is itself optimized granularly through a proxy, so a component that only reads errors.email does not rerender on every change to another field.


// components/SignupForm.jsx — React Hook Form: uncontrolled fields via refs, minimal rerenders
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'At least 8 characters'),
});

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm({ resolver: zodResolver(schema) });

  async function onSubmit(data) {
    await fetch('/api/signup', { method: 'POST', body: JSON.stringify(data) });
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <p role="alert">{errors.email.message}</p>}

      <input type="password" {...register('password')} />
      {errors.password && <p role="alert">{errors.password.message}</p>}

      <button type="submit" disabled={isSubmitting}>Sign up</button>
    </form>
  );
}

4. Formik in detail: established, but slower

Formik was the dominant form library in the React ecosystem for many years and is consistently built on controlled components: the Formik context holds values, errors and touched status for every field in React state, and the Field component connects an input element to that central state via value and onChange.

This architecture makes Formik conceptually easier to follow, because it stays closer to the classic React data flow with explicit state and props, but has a measurable performance downside: since every keystroke updates the central Formik state, the entire form tree rerenders on every input by default, unless explicitly optimized at field level with React.memo or the useField hook. On forms with more than roughly fifteen to twenty fields, this difference from React Hook Form becomes noticeable in practice, especially on weaker hardware or with complex validation rules that get re-evaluated on every change.


// components/SignupForm.jsx — Formik: controlled fields, central state re-renders the tree
import { Formik, Form, Field, ErrorMessage } from 'formik';
import { toFormikValidationSchema } from 'zod-formik-adapter';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'At least 8 characters'),
});

function SignupForm() {
  return (
    <Formik
      initialValues={{ email: '', password: '' }}
      validationSchema={toFormikValidationSchema(schema)}
      onSubmit={async (values) => {
        await fetch('/api/signup', { method: 'POST', body: JSON.stringify(values) });
      }}
    >
      {({ isSubmitting }) => (
        <Form>
          <Field name="email" />
          <ErrorMessage name="email" component="p" />

          <Field type="password" name="password" />
          <ErrorMessage name="password" component="p" />

          <button type="submit" disabled={isSubmitting}>Sign up</button>
        </Form>
      )}
    </Formik>
  );
}

5. TanStack Form in detail: headless and framework agnostic

TanStack Form is the youngest of the three libraries and follows the headless philosophy also found in TanStack Query and TanStack Table: no prebuilt UI components, but pure state and logic primitives that connect to any markup. The central useForm hook returns field API objects via form.Field, with each field getting its own, granularly isolated subscription scope.

A distinguishing feature of TanStack Form is its framework agnosticism: the same core library works not only with React, but also with Vue, Solid and Angular through thin adapter packages, which can be a relevant advantage for teams running multiple frontend frameworks within the same company. For pure React projects, TanStack Form offers performance comparable to React Hook Form, but with a more explicit API geared toward type inference, which delivers particularly precise types for nested form structures in TypeScript projects.


// components/SignupForm.jsx — TanStack Form: headless, field level subscriptions
import { useForm } from '@tanstack/react-form';
import { z } from 'zod';

const emailSchema = z.string().email('Invalid email address');

function SignupForm() {
  const form = useForm({
    defaultValues: { email: '', password: '' },
    onSubmit: async ({ value }) => {
      await fetch('/api/signup', { method: 'POST', body: JSON.stringify(value) });
    },
  });

  return (
    <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
      <form.Field
        name="email"
        validators={{ onChange: ({ value }) => emailSchema.safeParse(value).success ? undefined : 'Invalid email' }}
      >
        {(field) => (
          <>
            <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
            {field.state.meta.errors.length > 0 && <p role="alert">{field.state.meta.errors[0]}</p>}
          </>
        )}
      </form.Field>
      <button type="submit">Sign up</button>
    </form>
  );
}

6. Validation: Zod, Yup and schema based approaches

All three libraries have moved in recent years from their own proprietary validation APIs toward schema based validation with external libraries such as Zod or Yup. The benefit of this separation: the same Zod schema definition can be reused both client side for form validation and server side for validating incoming API requests, so validation logic does not need to be maintained twice.

React Hook Form integrates Zod directly into useForm through the official @hookform/resolvers adapter, TanStack Form accepts Zod schemas or arbitrary validator functions granularly per field or for the whole form, and Formik requires a community adapter for Zod support, since native Zod integration was only added later. In all three cases, a failed validation produces structured error messages that can be displayed directly next to the respective field, without having to implement error handling manually for each field separately.

7. Performance on large forms and field isolation

On forms with few fields, the choice of library barely matters for actual performance, all three approaches feel practically identical for ten fields or fewer. The difference only becomes relevant on extensive forms with fifty or more fields, for example in multi step configurators or complex settings pages with many nested options.

React Hook Form and TanStack Form isolate rerenders to field level by default, so a single keystroke in one field does not trigger rerenders in other, unchanged fields. Formik achieves the same isolation only with additional manual effort, through the useField hook combined with React.memo on individual field components. For applications with very large, dynamic forms involving field arrays, for example invoice line items with a variable count, the built in field isolation of React Hook Form and TanStack Form is a noticeable practical advantage over Formik.

8. Server actions and form state in React 19

React 19 introduced server actions and the new useActionState hook as a native, library independent alternative for simpler forms: a form can pass an async server function directly as the action prop, without needing an onSubmit handler or any of the three form libraries. For forms without complex client side live validation, the native React pattern is often sufficient and saves an extra dependency.

For forms with extensive client side validation while typing, dynamic field arrays or multi step wizard flows, however, a dedicated form library remains superior, because native server actions are primarily optimized for the submit moment and do not bring built in field by field validation while typing. React Hook Form also combines easily with server actions, since handleSubmit can forward the validated data to a server action, connecting client side validation with server side processing.

9. The three libraries compared directly

The table below summarizes the practically relevant differences.

Criterion React Hook Form Formik TanStack Form
Architecture Uncontrolled, refs Controlled, state per field Headless, granular subscriptions
Rerenders on large forms Minimal, field isolated Without optimization: whole tree Minimal, field isolated
Zod integration Official via resolver Only via community adapter Native per field or form
Framework agnosticism React only React only React, Vue, Solid, Angular
Maturity and ecosystem Very established Very established, older Newer, growing

Mironsoft

React architecture, state management and modern frontend infrastructure

A form that stutters on every keystroke?

We analyze existing forms, migrate from Formik to React Hook Form or TanStack Form, and connect Zod schemas consistently between client validation and the server endpoint.

Form migration

Move Formik or useState forms to React Hook Form or TanStack Form

Zod schema design

Build shared validation logic for client forms and server endpoints

Performance audit

Rerender analysis for large, dynamic forms with field arrays

10. Summary

The choice between React Hook Form, Formik and TanStack Form mainly depends on form size, the desired architectural model and any requirements for framework agnosticism. React Hook Form offers the best out of the box performance on large forms thanks to uncontrolled components and refs, plus an official Zod integration. Formik remains a solid, very established choice for smaller forms and teams that prefer the classic controlled approach, but requires additional optimization work on large forms.

TanStack Form impresses with granular field isolation on par with React Hook Form, plus the added benefit of framework agnosticism for teams running multiple frontend stacks. For simple forms without complex live validation, the native server action pattern via useActionState has also been a lean alternative since React 19, requiring no additional form library at all.

Form state libraries: the essentials at a glance

React Hook Form

Uncontrolled components via refs, minimal rerenders, official Zod integration via resolver.

Formik

Controlled components, very established, requires manual optimization for large forms.

TanStack Form

Headless, granular field isolation, framework agnostic across React, Vue, Solid and Angular.

Server actions

React 19 useActionState as a lean alternative for simple forms without live validation.

11. FAQ: Form State Libraries

1Main difference React Hook Form and Formik?
React Hook Form uses refs without rerenders, Formik uses central controlled state that rerenders on every input.
2When does TanStack Form pay off?
With framework agnosticism needs, or when a more explicit, more type safe API for nested forms is preferred.
3How do I integrate Zod with React Hook Form?
Via @hookform/resolvers as the resolver option in useForm, errors land automatically in formState.errors.
4Why does Formik rerender the whole tree?
Because of central controlled state. useField with React.memo per field optimizes this manually.
5Do I still need a library with server actions?
For simple forms useActionState is often enough, complex live validation still benefits from a dedicated library.
6What does controlled versus uncontrolled mean?
Controlled keeps the value in React state, uncontrolled lets the browser manage the value in the DOM.
7Best choice for very large forms with field arrays?
React Hook Form and TanStack Form, both isolate rerenders to field level by default.
8Can the same Zod schema be used for client and server?
Yes, the same definition validates client side and server side without duplicated logic.
9Is Formik outdated?
Not outdated, but less performant on large forms than newer approaches. Still solid for small forms.
10Does TanStack Form work outside React?
Yes, framework agnostic core library with adapters for Vue, Solid and Angular.