final-form, formik, react-hook-form, and redux-form are all libraries designed to manage form state, validation, and submission in React applications. They abstract away the boilerplate of tracking input values, handling errors, and managing submission logic, but they differ significantly in their underlying architecture, performance characteristics, and integration patterns with React.
Managing forms in React involves tracking values, handling validation, displaying errors, and managing submission — all while avoiding unnecessary re-renders. The four libraries under comparison take different approaches to this problem. Let’s examine how they handle core concerns like state management, validation, and performance.
final-form keeps state outside React, in a standalone store. Components subscribe only to the fields they care about, minimizing re-renders.
// final-form: Create form outside component
import { createForm } from 'final-form';
const form = createForm({
onSubmit: values => console.log(values)
});
// In component
const { blur, change, focus } = form;
const state = form.getState();
formik stores state inside React context. The entire form re-renders on any field change unless you memoize or split components.
// formik: Form state lives in React context
import { Formik, Field, Form } from 'formik';
<Formik
initialValues={{ email: '' }}
onSubmit={values => console.log(values)}
>
<Form>
<Field name="email" />
</Form>
</Formik>
react-hook-form avoids React state for inputs by using uncontrolled components and reading values directly from the DOM via refs.
// react-hook-form: Leverages uncontrolled inputs
import { useForm } from 'react-hook-form';
function MyForm() {
const { register, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit(console.log)}>
<input {...register('email')} />
</form>
);
}
redux-form (deprecated) stores all form state in the global Redux store, causing frequent re-renders and tight coupling to Redux.
// redux-form: Form state in Redux store (DO NOT USE)
import { reduxForm, Field } from 'redux-form';
const MyForm = () => <Field name="email" component="input" />;
export default reduxForm({ form: 'myForm' })(MyForm);
⚠️ Important:
redux-formis officially deprecated. The npm page states: "This library is no longer maintained. Please use Final Form or React Hook Form instead."
All libraries support synchronous and asynchronous validation, but their APIs differ.
final-form accepts a validate function that returns an error object. Async validation requires manual handling via onSubmit or custom logic.
const form = createForm({
validate: values => {
const errors = {};
if (!values.email) errors.email = 'Required';
return errors;
}
});
formik supports both sync and async validation via validate or validationSchema (with Yup).
<Formik
validate={values => {
const errors = {};
if (!values.email) errors.email = 'Required';
return errors;
}}
>
{/* ... */}
</Formik>
react-hook-form offers multiple validation strategies: built-in HTML5 validation, schema-based (Zod, Yup), or custom functions via register.
const { register } = useForm();
<input {...register('email', { required: 'Email is required' })} />;
redux-form used validate prop similarly to Formik, but tied validation results to Redux state.
// redux-form (deprecated)
const validate = values => {
const errors = {};
if (!values.email) errors.email = 'Required';
return errors;
};
export default reduxForm({ form: 'myForm', validate })(MyForm);
final-form excels at performance by allowing components to subscribe only to specific fields. Only subscribed fields trigger re-renders.
// Subscribe only to 'email' changes
const unsubscribe = form.subscribe(
state => console.log(state.values.email),
{ values: true }
);
formik causes the entire form subtree to re-render on any field change unless you split the form into smaller components or use useField with memoization.
// Without optimization, entire form re-renders
const MyInput = ({ name }) => {
const [field] = useField(name);
return <input {...field} />;
};
react-hook-form avoids re-renders entirely for input changes because it doesn’t track values in React state. Only explicit state updates (e.g., errors) cause re-renders.
// Input changes don't trigger re-renders
const { register, formState } = useForm();
// formState.errors changes → re-render; input typing → no re-render
redux-form caused excessive re-renders because every keystroke updated the global Redux store, triggering connected components to update.
final-form is framework-agnostic. You must build your own React integration using hooks or context.
// Custom hook for final-form
function useField(form, name) {
const [state, setState] = useState(form.getState());
useEffect(() => {
return form.subscribe(setState, { values: true, errors: true });
}, [form, name]);
return state;
}
formik provides both render props (<Formik>) and hooks (useFormik). The hook version is now preferred.
const formik = useFormik({
initialValues: { email: '' },
onSubmit: console.log
});
react-hook-form is built entirely around hooks, aligning with modern React patterns.
const { register, handleSubmit, formState } = useForm();
redux-form relied heavily on higher-order components (HOCs) and Redux connect patterns, which feel outdated in today’s React ecosystem.
Adding/removing fields dynamically is common in complex forms.
final-form provides mutators for array operations (e.g., push, remove).
import { createForm } from 'final-form';
import { arrayMutators } from 'final-form-arrays';
const form = createForm({ mutators: { ...arrayMutators } });
form.mutators.push('emails', '');
formik includes push, remove, and other array helpers via FieldArray.
<FieldArray name="emails">
{({ push, remove, form }) => (
<div>
{form.values.emails.map((_, i) => (
<Field name={`emails[${i}]`} />
))}
<button onClick={() => push('')}>Add</button>
</div>
)}
</FieldArray>
react-hook-form uses useFieldArray for dynamic lists, which manages refs and state efficiently.
const { fields, append, remove } = useFieldArray({
name: 'emails'
});
return (
<div>
{fields.map((field, i) => (
<input key={field.id} {...register(`emails.${i}`)} />
))}
<button onClick={() => append('')}>Add</button>
</div>
);
redux-form supported dynamic fields via Fields and array actions, but required dispatching Redux actions.
| Feature | final-form | formik | react-hook-form | redux-form |
|---|---|---|---|---|
| State Location | External store | React context | DOM refs + minimal state | Redux store |
| Re-render Strategy | Field subscriptions | Full form re-render | Minimal (uncontrolled) | Global Redux updates |
| Primary API | Imperative + subscriptions | Hooks / render props | Hooks | HOCs + Redux actions |
| Dynamic Fields | Mutators | FieldArray | useFieldArray | Redux array actions |
| Maintenance Status | Actively maintained | Actively maintained | Actively maintained | Deprecated |
react-hook-form or final-form.formik.final-form.redux-form — it’s deprecated.Choose based on your team’s familiarity, performance needs, and how much control you want over the rendering layer. All three active libraries are solid choices — just match the tool to your constraints.
Choose react-hook-form if you prioritize performance and minimal re-renders by leveraging uncontrolled components and native HTML form validation. It’s especially effective for large or dynamic forms where every re-render matters. Its hook-based API integrates cleanly with modern React patterns and reduces boilerplate by avoiding unnecessary state synchronization between React and the DOM.
Choose formik if you prefer a higher-level, component-driven API that wraps your form in a context provider and uses render props or hooks to expose form state and helpers. It’s well-suited for teams that value convention over configuration and don’t have extreme performance requirements. Its declarative validation and built-in support for nested objects make it approachable for medium-complexity forms.
Choose final-form if you need a framework-agnostic, subscription-based form state manager that minimizes re-renders through fine-grained field subscriptions. It’s ideal when you want full control over rendering and performance optimization without tying your form logic to React’s component lifecycle. However, be prepared to write more integration code since it doesn’t provide React-specific helpers out of the box.
Do not choose redux-form for new projects. It is officially deprecated and no longer maintained, as stated on its npm page and GitHub repository. While it was once popular for deeply integrating form state into Redux stores, its tight coupling to Redux, high re-render overhead, and maintenance burden make it unsuitable for modern applications. Migrate existing implementations to alternatives like react-hook-form or formik.
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