I used Formik for years. It was the obvious choice from roughly 2018 to 2021. Then react-hook-form became the community standard and I switched. Here's why that happened and whether it was the right call.

Formik's Approach

Formik manages forms using controlled inputs. Every keystroke triggers a state update. The form state lives in React state:

import { Formik, Form, Field, ErrorMessage } from 'formik'

<Formik
  initialValues={{ email: '', password: '' }}
  validate={values => {
    const errors = {}
    if (!values.email) errors.email = 'Required'
    return errors
  }}
  onSubmit={(values) => console.log(values)}
>
  {({ isSubmitting }) => (
    <Form>
      <Field type="email" name="email" />
      <ErrorMessage name="email" component="div" />
      <button type="submit" disabled={isSubmitting}>Submit</button>
    </Form>
  )}
</Formik>

Formik's API is intuitive. Field, Form, ErrorMessage — the names match what they do. The render prop pattern was clean.

Formik's problem: every keystroke re-renders the entire form. On a complex form with 20 fields this causes noticeable performance issues.

react-hook-form's Approach

react-hook-form uses uncontrolled inputs by default. Instead of tracking every value in state, it reads values from the DOM when needed:

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'),
  password: z.string().min(6, 'Min 6 characters'),
})

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

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}
      <button type="submit">Submit</button>
    </form>
  )
}

Why this is better:

  • Uncontrolled inputs = no re-render on each keystroke = faster, especially for large forms
  • register just sets ref, name, onChange, onBlur — minimal overhead
  • Built-in zod integration via @hookform/resolvers
  • TypeScript inference from zod schema is excellent

The zod Integration

This is honestly what made me switch permanently. The combination of react-hook-form + zod is the best form validation experience I've had in any framework:

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ['confirmPassword']
})

type FormData = z.infer<typeof schema> // TypeScript type automatically generated

const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
  resolver: zodResolver(schema)
})

You define the schema once. TypeScript types are derived from it. Validation runs against it. Error messages come from it. No duplication, no type drift.

Formik also has zod integration via yup or directly, but it's not as clean and z.infer<typeof schema> doesn't type formState.errors as precisely.

shadcn/ui Integration

shadcn/ui built its Form components specifically for react-hook-form:

import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'

<Form {...form}>
  <FormField
    control={form.control}
    name="email"
    render={({ field }) => (
      <FormItem>
        <FormLabel>Email</FormLabel>
        <FormControl>
          <Input {...field} type="email" />
        </FormControl>
        <FormMessage /> {/* Automatically shows error */}
      </FormItem>
    )}
  />
</Form>

FormMessage reads errors from the form context automatically. No manual error display. This is genuinely excellent DX.

Bundle Size

react-hook-form: ~25KB minified Formik: ~45KB minified

Not a massive difference but react-hook-form wins.

When to Keep Using Formik

If you have a large existing Formik codebase that works, don't migrate for the sake of it. The migration is mechanical but not trivial for a large form library. Formik works fine.

For new projects: react-hook-form. The performance advantage, TypeScript support, zod integration and shadcn/ui compatibility make it the clear choice in 2026.

Frequently Asked Questions

For new projects in 2026, yes. react-hook-form has better performance (uncontrolled inputs by default), a cleaner API, better TypeScript support, native zod integration via @hookform/resolvers and smaller bundle size. Formik still works fine but react-hook-form is the current community standard.
Formik is maintained but has slowed significantly. The last major release was v2.x. The author acknowledged in 2023 that development has slowed. For existing Formik projects this is fine. For new projects react-hook-form is the safer long-term choice.
Install @hookform/resolvers and zod. Define a schema: const schema = z.object({ email: z.string().email() }). Pass to useForm: useForm({ resolver: zodResolver(schema) }). Zod handles validation, react-hook-form handles form state.
Yes, perfectly. shadcn/ui's Form components are built specifically for react-hook-form integration. Use FormField, FormItem, FormLabel, FormControl and FormMessage to wire up shadcn inputs with react-hook-form automatically.

Related Comparisons

Already Decided on Bootstrap?

Get a complete Angular 21 + Bootstrap 5 admin dashboard template — production ready.

Browse Templates →

Use code FIRST30 for 30% off.