formik, react-final-form, react-hook-form, and react-use-form-state are libraries designed to handle the complexity of form state, validation, and submission in React applications. While native React handles simple inputs well, these tools solve specific pain points like managing nested values, displaying error messages, tracking touched states, and preventing unnecessary re-renders. formik offers a component-based API with built-in validation helpers. react-final-form uses a render-prop pattern backed by a framework-agnostic core. react-hook-form leverages React hooks and uncontrolled components to minimize re-renders. react-use-form-state provides a lightweight hook-based approach for simpler state management needs.
Managing forms in React often becomes a bottleneck for performance and code maintainability. While React's built-in state management works for simple inputs, real-world applications require handling validation, error display, submission states, and complex nested data. The four libraries—formik, react-final-form, react-hook-form, and react-use-form-state—approach these problems with fundamentally different architectures. Let's break down how they work under the hood.
The biggest difference between these libraries is how they handle input values. This choice directly impacts how often your components re-render.
formik uses controlled components. It stores the entire form state in a single React state object. Every time you type a character, the state updates, and the component re-renders.
// formik: Controlled input
import { useFormik } from 'formik';
function MyForm() {
const formik = useFormik({
initialValues: { email: '' },
onSubmit: (values) => console.log(values),
});
return (
<form onSubmit={formik.handleSubmit}>
<input
name="email"
value={formik.values.email}
onChange={formik.handleChange}
/>
</form>
);
}
react-final-form also uses controlled components by default when using its <Field> component. It subscribes components to specific slices of state, but the input value is still driven by React state.
// react-final-form: Controlled via Field component
import { Form, Field } from 'react-final-form';
function MyForm() {
return (
<Form onSubmit={({ email }) => console.log(email)}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field name="email">
{({ input, meta }) => (
<input {...input} />
)}
</Field>
</form>
)}
</Form>
);
}
react-hook-form uses uncontrolled components. It does not store input values in React state. Instead, it registers refs to the DOM nodes and reads values directly from the browser when needed (like on submit).
// react-hook-form: Uncontrolled input with ref
import { useForm } from 'react-hook-form';
function MyForm() {
const { register, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("email")} />
</form>
);
}
react-use-form-state uses controlled components but simplifies the API by returning a state object and event handlers directly from a hook.
// react-use-form-state: Controlled via hook return
import { useFormState } from 'react-use-form-state';
function MyForm() {
const [formState, { email }] = useFormState({ email: '' });
return (
<form onSubmit={(e) => {
e.preventDefault();
console.log(formState.values);
}}>
<input {...email()} />
</form>
);
}
Performance is where the architectural choices become visible. If you have a form with 50 inputs, how does each library handle typing in one of them?
formik triggers a re-render of the entire form component on every keystroke because the main state object changes. You must manually optimize this using React.memo or splitting fields into separate components.
// formik: Potential performance bottleneck
// Typing here causes MyForm to re-render entirely
<input name="email" value={formik.values.email} onChange={formik.handleChange} />
react-final-form mitigates this by allowing you to subscribe only to specific fields. The <Field> component isolates updates, so typing in one input doesn't re-render the whole form tree.
// react-final-form: Isolated updates
// Only the Field component re-renders, not the parent Form
<Field name="email">{({ input }) => <input {...input} />}</Field>
react-hook-form offers the best performance out of the box. Since it doesn't use React state for values, typing does not trigger any React re-renders. Re-renders only happen if you explicitly watch a field or if validation errors change.
// react-hook-form: No re-renders on typing
// The component stays static until submit or error state changes
<input {...register("email")} />
react-use-form-state behaves similarly to standard React state. Typing updates the state and triggers a re-render of the component using the hook. It is efficient for small forms but lacks the isolation mechanisms of react-final-form or the uncontrolled nature of react-hook-form.
// react-use-form-state: Standard React re-render cycle
// Component re-renders on every state update
<input {...email()} />
Validation is critical for user experience. Each library provides a different way to define and display errors.
formik supports both schema-based validation (using Yup) and custom functions. It automatically maps errors to field names.
// formik: Schema validation with Yup
import * as Yup from 'yup';
const formik = useFormik({
initialValues: { email: '' },
validationSchema: Yup.object({
email: Yup.string().email('Invalid email').required('Required')
}),
onSubmit: (values) => {}
});
// Access error: formik.errors.email
react-final-form allows validation at the form level or the field level. Field-level validation is powerful because it keeps validation logic close to the input.
// react-final-form: Field-level validation
<Field
name="email"
validate={value =>
value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)
? 'Invalid email'
: undefined
}
>
{({ input, meta }) => (
<>
<input {...input} />
{meta.error && meta.touched && <span>{meta.error}</span>}
</>
)}
</Field>
react-hook-form integrates with external schema libraries (like Yup or Zod) but also supports native HTML5 validation. It registers rules directly in the register function.
// react-hook-form: Register with rules
<input
{...register("email", {
required: "Required",
pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, message: "Invalid email" }
})}
/>
// Access error: errors.email?.message
react-use-form-state relies on you to write custom validation logic. It does not have a built-in validation engine. You must manually update the state or handle errors in your submit handler.
// react-use-form-state: Manual validation
const [formState, { email }] = useFormState({ email: '' }, {
validate: (state) => {
const errors = {};
if (!state.values.email.includes('@')) errors.email = 'Invalid';
return errors;
}
});
Real-world forms often include dynamic lists (e.g., adding multiple phone numbers) or nested data.
formik excels here with helper methods like arrayHelpers to push, pop, or swap items in an array while maintaining state integrity.
// formik: Array helpers
<FieldArray name="friends">
{({ push, remove }) => (
<div>
{formik.values.friends.map((friend, index) => (
<div key={index}>
<Field name={`friends.${index}.name`} />
<button type="button" onClick={() => remove(index)}>X</button>
</div>
))}
<button type="button" onClick={() => push({ name: '' })}>Add Friend</button>
</div>
)}
</FieldArray>
react-final-form handles arrays using the <FieldArray> component (from react-final-form-arrays). It works similarly to Formik but uses the render-prop pattern.
// react-final-form: FieldArray component
<FieldArray name="friends">
{({ fields }) => (
<div>
{fields.map((name, index) => (
<div key={name}>
<Field name={`${name}.name`} component="input" />
<button type="button" onClick={() => fields.remove(index)}>X</button>
</div>
))}
<button type="button" onClick={() => fields.push({ name: '' })}>Add</button>
</div>
)}
</FieldArray>
react-hook-form uses the useFieldArray hook. It is optimized for performance, ensuring that adding/removing items doesn't cause unnecessary re-renders of unaffected fields.
// react-hook-form: useFieldArray hook
const { fields, append, remove } = useFieldArray({
control,
name: "friends"
});
{fields.map((item, index) => (
<div key={item.id}>
<input {...register(`friends.${index}.name`)} />
<button type="button" onClick={() => remove(index)}>X</button>
</div>
))}
<button type="button" onClick={() => append({ name: '' })}>Add</button>
react-use-form-state does not have built-in helpers for complex arrays. You must manage array manipulation manually using standard JavaScript array methods and update the form state.
// react-use-form-state: Manual array management
const addFriend = () => {
formState.setValues({
...formState.values,
friends: [...formState.values.friends, { name: '' }]
});
};
Before choosing a library, you must consider its long-term viability.
react-use-form-state is effectively deprecated and no longer maintained. The repository has seen no significant activity for years, and the author recommends against using it for new projects. It lacks the features and performance optimizations found in modern alternatives.
⚠️ Recommendation: Do not use
react-use-form-statein new production applications. Evaluatereact-hook-formorformikinstead.
The other three libraries (formik, react-final-form, react-hook-form) are actively maintained and widely used in the industry.
Despite their differences, these libraries solve the same core problems.
All libraries provide a way to intercept form submission, prevent default browser behavior, and gather data.
// All libraries eventually provide a 'values' object on submit
onSubmit: (values) => {
api.post('/submit', values);
}
Each library exposes an error state that allows you to conditionally render error messages near inputs.
// Common pattern across all
{errors.email && <span className="error">{errors.email}</span>}
They all track whether a user has interacted with a field (touched), allowing you to show errors only after the user leaves the input.
// Only show error if field was touched
{touched.email && errors.email && <div>{errors.email}</div>}
| Feature | formik | react-final-form | react-hook-form | react-use-form-state |
|---|---|---|---|---|
| Architecture | Controlled (React State) | Controlled (Subscription) | Uncontrolled (Refs) | Controlled (React State) |
| Re-renders | High (per keystroke) | Low (isolated fields) | Minimal (no state updates) | High (per keystroke) |
| API Style | Hooks & Components | Render Props & Components | Hooks | Hooks |
| Validation | Built-in + Yup | Field/Form Level | Built-in + External | Manual |
| Arrays | FieldArray helpers | FieldArray component | useFieldArray hook | Manual JS |
| Status | ✅ Active | ✅ Active | ✅ Active | ⚠️ Deprecated |
formik is the safe, conventional choice. It has the largest community and the most tutorials. If your team values convention over configuration and doesn't have extreme performance needs, it is a solid choice.
react-final-form is the power-user tool. It offers the most flexibility for complex validation logic and enterprise-grade requirements. Choose this if you need fine-grained control over rendering and validation scopes.
react-hook-form is the performance leader. By leveraging uncontrolled components, it eliminates the re-render bottleneck inherent in React forms. It is the best choice for large forms, dashboards, or applications running on slower devices.
react-use-form-state should be avoided for new projects. Its lack of maintenance and limited feature set make it a risky dependency compared to the robust alternatives available today.
Final Thought: The industry is shifting towards react-hook-form for its performance benefits and modern hook-based API. However, formik remains a strong contender for teams that prefer its explicit component structure. Avoid deprecated tools and choose based on your specific performance and complexity needs.
Choose formik if your team prefers a component-based API (using <Form> and <Field>) and needs a mature ecosystem with extensive community examples. It is ideal for complex forms where you want built-in helpers for handling nested objects and arrays without writing custom logic. However, be aware that it triggers re-renders on every keystroke by default, which may require optimization for very large forms.
Choose react-final-form if you need a robust, framework-agnostic core that supports advanced features like field-level validation, asynchronous validation, and form wizards. It is best suited for enterprise applications where strict separation of concerns and high testability are critical. The render-prop API provides maximum flexibility but can lead to deeper component trees compared to hook-based solutions.
Choose react-hook-form if performance is your top priority and you want to avoid the re-render overhead associated with controlled components. It is the best fit for large, dynamic forms or low-end devices because it relies on uncontrolled inputs and native browser validation. Select this if your team is comfortable using ref registration and prefers a hook-centric API over wrapper components.
Choose react-use-form-state only for small to medium-sized projects where you need a minimal, hook-based solution without the bloat of a full framework. It is suitable when you want simple state management and validation without learning a complex new API surface. Avoid this for highly complex forms with dynamic fields or advanced validation requirements, as it lacks the specialized features of the other three libraries.
Visit https://formik.org to get started with Formik.
List of organizations and projects using Formik
This monorepo uses yarn, so to start you'll need the package manager installed.
To run E2E tests you'll also need Playwright set up, which can be done locally via npx playwright install. Afterward, run yarn start:app and in a separate tab run yarn e2e:ui to boot up the test runner.
When you're done with your changes, we use changesets to manage release notes. Run yarn changeset to autogenerate notes to be appended to your pull request.
Thank you!
Formik is made with <3 thanks to these wonderful people (emoji key):
Jared Palmer 💬 💻 🎨 📖 💡 🤔 👀 ⚠️ | Ian White 💬 🐛 💻 📖 🤔 👀 | Andrej Badin 💬 🐛 📖 | Adam Howard 💬 🐛 🤔 👀 | Vlad Shcherbin 💬 🐛 🤔 | Brikou CARRE 🐛 📖 | Sam Kvale 🐛 💻 ⚠️ |
|---|---|---|---|---|---|---|
Jon Tansey 🐛 💻 | Tyler Martinez 🐛 📖 | Tobias Lohse 🐛 💻 |
This project follows the all-contributors specification. Contributions of any kind welcome!