final-form, formik, and react-hook-form are popular libraries for managing form state, validation, and submission in React applications. They abstract away the boilerplate of handling user input, displaying errors, and synchronizing data across form fields. Each takes a different architectural approach: final-form uses a subscription-based model with state outside React, formik relies on React component state with controlled inputs, and react-hook-form leverages uncontrolled components with refs for optimal performance.
Managing form state in React apps is a common but surprisingly tricky problem. While you could roll your own with useState, real-world forms need validation, error handling, performance optimization, and often complex nested structures. The three libraries — final-form, formik, and react-hook-form — each solve this problem differently. Let’s compare how they actually work in practice.
final-form keeps form state outside React, in a standalone JavaScript object. It uses subscriptions to notify components only when their specific fields change. This avoids unnecessary re-renders.
import { createForm } from 'final-form';
const form = createForm({
onSubmit: values => console.log(values)
});
// Subscribe to field changes
const unsubscribe = form.subscribe(
state => console.log('Field value:', state.values.email),
{ values: true }
);
formik stores all form state inside React component state (via useState or useReducer). Every field change triggers a full re-render of the form unless you optimize with React.memo.
import { useFormik } from 'formik';
function MyForm() {
const formik = useFormik({
initialValues: { email: '' },
onSubmit: values => console.log(values)
});
return (
<form onSubmit={formik.handleSubmit}>
<input
name="email"
onChange={formik.handleChange}
value={formik.values.email}
/>
</form>
);
}
react-hook-form embraces uncontrolled components by default. It uses refs to read input values directly from the DOM when needed, minimizing re-renders and avoiding state synchronization overhead.
import { useForm } from 'react-hook-form';
function MyForm() {
const { register, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input {...register('email')} />
</form>
);
}
All three support synchronous and asynchronous validation, but their timing differs.
final-form validates on blur, change, or submit based on configuration. You provide validator functions that return error messages.
const form = createForm({
validate: values => {
const errors = {};
if (!values.email) errors.email = 'Required';
return errors;
}
});
formik runs validation on every change by default (can be tuned). Validators can be functions or integrated with libraries like Yup.
import * as Yup from 'yup';
const formik = useFormik({
validationSchema: Yup.object({
email: Yup.string().required('Required')
})
});
react-hook-form validates on blur, change, or submit (configurable per field). It supports both built-in rules and resolver-based validation (e.g., with Zod or Yup).
const { register } = useForm({
defaultValues: { email: '' }
});
<input
{...register('email', {
required: 'Required',
pattern: {
value: /@/,
message: 'Must be valid email'
}
})}
/>;
Real forms often include lists (e.g., “add more contacts”) or deeply nested data.
final-form handles arrays via its FieldArray component. You manage array operations manually (push, remove, etc.).
import { FieldArray } from 'final-form-arrays';
<FieldArray name="contacts">
{({ fields }) => (
<div>
{fields.map((name, index) => (
<input key={name} name={`${name}.email`} />
))}
<button onClick={() => fields.push({ email: '' })}>Add</button>
</div>
)}
</FieldArray>
formik provides useFieldArray (or FieldArray component) with built-in helpers for array manipulation.
import { useFieldArray } from 'formik';
function ContactList() {
const { fields, append, remove } = useFieldArray({ name: 'contacts' });
return (
<div>
{fields.map((field, index) => (
<input key={field.id} name={`contacts[${index}].email`} />
))}
<button onClick={() => append({ email: '' })}>Add</button>
</div>
);
}
react-hook-form offers useFieldArray with similar array helpers, but leverages uncontrolled inputs under the hood.
import { useFieldArray } from 'react-hook-form';
function ContactList({ control }) {
const { fields, append, remove } = useFieldArray({
control,
name: 'contacts'
});
return (
<div>
{fields.map((field, index) => (
<input key={field.id} {...register(`contacts.${index}.email`)} />
))}
<button onClick={() => append({ email: '' })}>Add</button>
</div>
);
}
final-form minimizes re-renders through fine-grained subscriptions. Only components listening to changed fields update.
formik re-renders the entire form on every keystroke by default. Mitigation requires manual memoization (React.memo, useCallback) or splitting into smaller components.
react-hook-form avoids most re-renders because inputs are uncontrolled. Only explicit state changes (like showing errors) trigger updates.
All three work with popular component libraries (Material UI, Ant Design, etc.), but integration patterns differ:
final-form: Often used with react-final-form wrapper for React bindings.formik: Directly compatible; many UI libraries provide Formik-specific adapters.react-hook-form: Uses controller wrappers (Controller component or useController hook) for controlled third-party inputs.// react-hook-form with Material UI
import { Controller } from 'react-hook-form';
import TextField from '@mui/material/TextField';
<Controller
name="email"
control={control}
render={({ field }) => <TextField {...field} />}
/>;
final-form: Steeper learning curve due to subscription model and separation from React state. Best for teams comfortable with external state management.formik: Gentle onboarding for React developers — feels like “just more React.” Can become verbose in large forms.react-hook-form: Requires shifting mindset to uncontrolled components. Very concise for simple forms, but needs extra setup for controlled inputs.final-form: Mature but less actively evolved. Still maintained, no deprecation notices.formik: Actively maintained with strong community adoption. Regular updates and TypeScript support.react-hook-form: Rapidly growing ecosystem, excellent TypeScript support, and frequent updates focused on performance and DX.final-form if you’re building very large, performance-critical forms and are comfortable managing state outside React.formik if your team prefers a React-native approach, you’re already using Yup for validation, or you need quick prototyping.react-hook-form if you prioritize performance out of the box, want minimal re-renders, and are okay with uncontrolled inputs or wrapping controlled components.Choose react-hook-form if you prioritize performance and minimal re-renders by default, especially in forms with many fields. Its uncontrolled component model reduces boilerplate and aligns well with modern React practices. It's particularly strong when paired with validation resolvers like Zod or Yup, though you'll need to wrap controlled third-party inputs using its Controller API.
Choose formik if your team prefers a straightforward, React-native approach using controlled components and component state. It integrates seamlessly with validation libraries like Yup and works well for small to medium forms where performance isn't the primary bottleneck. Avoid it for very large forms unless you're willing to invest in optimization techniques like memoization.
Choose final-form if you need maximum performance in large, complex forms and are comfortable managing form state outside of React's component tree. Its subscription model minimizes unnecessary re-renders, making it ideal for enterprise applications where form responsiveness is critical. However, be prepared for a steeper learning curve and more manual setup compared to React-centric alternatives.
Get started | API | Form Builder | FAQs | Examples
npm install react-hook-form
import { useForm } from 'react-hook-form';
function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register('firstName')} />
<input {...register('lastName', { required: true })} />
{errors.lastName && <p>Last name is required.</p>}
<input {...register('age', { pattern: /\d+/ })} />
{errors.age && <p>Please enter number for age.</p>}
<input type="submit" />
</form>
);
}
We’re incredibly grateful to these kind and generous sponsors for their support!
Thank you to our previous sponsors for your generous support!
Thanks go to all our backers! [Become a backer].
Thanks go to these wonderful people! [Become a contributor].
Documentation website supported and backed by Vercel