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 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.
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 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.
You build great forms, but do you know HOW users use your forms? Find out with Form Nerd! Professional analytics from the creator of Final Form.
π° Hey there! Do you fancy yourself a javascript expert? Take this quiz and get offers from top tech companies! π°
β Zero dependencies
β Framework agnostic
β Opt-in subscriptions - only update on the state you need!
β π₯ 5.1k gzipped π₯
Comprehensive JS framework and UI components for building enterprise-grade web apps.
In the interest of making π Final Form the best library it can be, we'd love your thoughts and feedback.
