
Formik, the strongest React Hook form library of yesteryear, and React Hook Form, the strongest form library of today. These are two of the most popular form control libraries for React. These libraries provide simple and extensible ways to create, manage, and validate form fields, saving devs valuable time and preventing the headache of dealing with traditional form handling when building out user flows.
The Beef

React Hook Form
(RHF) became the de facto industry standard for form management in React. If you compare historical downloads across both libraries, you'll notice that for a long time,Formik
dominated the space- until eventually RHF overtook it. This shift happened right around the time I started learning React.call a hook
orrender a component
that sets up acontext
which serves as the initial entry point to the library. Users can then usespecialty functions
to bind inputs to that context, and pass acallback function
triggered when the form is submitted. As I became more familiar with how state works in React, I realized whyRHF
no-diffedFormik
in the end.RHF has a pretty simple and intuitive structure that makes it easy to employ when setting up simple forms. The primary entry point is the useForm
hook, which returns utility methods for managing form state.
import { useForm } from 'react-hook-form';
const myApp = () => {
const { register, handleSubmit } = useForm();
//...any other stuff.
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('firstName')} />
<input {...register('lastName')} />
<input type="submit" />
</form>
)
}
The two key components here are register
and handleSubmit
. handleSubmit
takes a callback function that is called when the form is submitted and fully valid.
It provides the callback with information on the form values, the form state, and other useful information. register
is a function that returns an object with a ref
property that is used to bind the input to the form state.
There's a couple of other useful methods that useForm
provides, but these are the two most important ones.
Formik is a little older than RHF, and it has a different approach to form management. Instead of "registering" components by handling their refs under the hood, Formik uses a controlled component approach.
The primary entrypoint for this library is a Formik
component, which you wrap around the form components you want to manage.. This is a higher order component that provides an object composed of form state and utility methods to manage that state (sounds familiar hmm). Here's an example ripped straight from the docs.
import { Formik, Form, Field } from 'formik';
const myApp = () => {
return (
<Formik
initialValues={{ firstName: '', lastName: '' }}
onSubmit={values => console.log(values)}
>
<Form>
<input
type="email"
name="email"
onChange={handleChange}
onBlur={handleBlur}
value={values.email}
/>
{errors.email && touched.email && errors.email}
<input
type="password"
name="password"
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
/>
<button type="submit">Submit</button>
</Form>
</Formik>
)
}
Like RHF, formik is able to take a callback function that is called on submit, and is provided with the form values. However, unlike RHF, actually controlling the input fields is left to the developer.
These two examples show both libraries in their most basic usecase, and for most intents and purposes they don't seem all that different. If you try either of these forms out locally you won't notice much of a performance difference. However, the differences between these two libraries become more apparent when you start to build out more complex forms.
The Invisible Hand of the Formik Rerender Economy
Say that you're building an online scientific journal website that accepts submissions from researchers in the community. Verified users can submit articles to the site by attaching a file, entering an abstract, and filling out a form with their personal information. This form has a lot of fields, and some of them are required. One of the fields for the form allows researchers to enter a list of authors/co-authors for the article.
This field is a list of AuthorInput
components. Users can input the first name, last name, and email of each author. Let's build out a super basic version of this form using both libraries and see how they compare.
Formik Example Sandbox
React Hook Form Example Sandbox
"open in new window"
near the top right of the preview, you'll notice we are using a handy tool named react-scan (link to repo). This is a performance monitoring tool that allows us to identify component re-renders using simple visual cues. Super handy tool that I've been using a lot lately to identify performance bottlenecks in my code.entire author component re-renders for each keystroke
. In fact, if you try adding a bunch of authors and then type into any of the fields, theentire form
re-renders for each keystroke.
RHF
example,no components rerender
regardless of the amout of user keystrokes (except when you push a new author, during which you'll notice AuthorComponents rerender due to the changing contents of the list).
React.memo
or if you use theReact Compiler
(yeah i watched that Theo video)) and/or using components likeFastField
, you're going to be fighting demons trying to nest two FieldArrays without grinding your form to a halt.Why the difference?
Why does Formik rerender so much? The answer is pretty straightforward. Formik uses a controlled component approach, which means that technically every time an input is modified, the entire form state is updated. If you take a look at the Formik source code, you'll see that most of the state management happens in the
context
spawned by theuseFormik
hook and revolves around a single ref, stateRef
. Methods that get passed to the form components, like handleChange
, handleBlur
, and setFieldValue
, internally call a dispatch
callback, which takes in an action that is then passed into an internal reducer that figures out what operation (update, read, validate, etc.) needs to be performed on stateRef
. Once the operation is performed, an internal iteration
counter is incremented. This iteration
is based on a useState()
call, so when it changes, a re-render is triggered. This ref then gets destructured on the return statement of the useFormik
hook, thus literally redefining the entire form values state on every update.
export function useFormik(
{
/**...*/
},
) {
// Bunch of extra state for stuff like validation, utility functions for FieldArrays, etc...
// ...
// We care about THIS
const stateRef = React.useRef({
values: cloneDeep(props.initialValues),
errors: cloneDeep(props.initialErrors) || emptyErrors,
touched: cloneDeep(props.initialTouched) || emptyTouched,
status: cloneDeep(props.initialStatus),
isSubmitting: false,
isValidating: false,
submitCount: 0,
});
const state = stateRef.current;
// Big Humongous Ginormous Reducer function that handles all the state updates to `stateRef`
// State reducer
function formikReducer(state, msg) {
switch (msg.type) {
case "SET_VALUES":
return { ...state, values: msg.payload };
case "SET_TOUCHED":
return { ...state, touched: msg.payload };
case "SET_ERRORS":
if (isEqual(state.errors, msg.payload)) {
return state;
}
// etc
// ...
}
// ...
// Dispatch function that sends actions to the `formikReducer`, which is responsible for updating the ref and incrementing the iteration.
const dispatch = React.useCallback((action) => {
const prev = stateRef.current;
stateRef.current = formikReducer(prev, action);
// force rerender
if (prev !== stateRef.current) setIteration((x) => x + 1);
}, []);
// Even more stuff, handleChange, onBlur, onFocus handlers, etc.
// Finally, formik returns the form state and any other fields provided
// by the library
return {
...state,
// ...
// And a whole lot more
};
}
}
It's really easy to shoot yourself in the foot when you're building something complex with Formik. If you're clever (I'm not fyi) you can get around a lot of this with some memoization and good component design. Of course, it's really only worth going for memoization if your form is big enough to outweigh the immediate performance cost of memoization. If you're building a small form, you might not need to optimize for less re-renders.
React Hook Form, on the other hand, uses an uncontrolled component approach. RHF yoinks the ref from the input component, and passes in all of the necessary callbacks (onChange, onBlur, onFocus. etc.) via the register
function. The input callbacks then save whatever values the user inputs into an additional ref named _formValues
(similar to the values
field in formik) however RHF doesn't trigger a state update unless it needs to.
For instance, lets look at what it takes to watch
a field value using the useWatch
hook. Internally, RHF employs an observer pattern in order to determine whether or not a state update needs to be triggered. Scattered around the RHF codebase, you'll see various mentions of _subjects.state.next({})
, watch.next({})
, array.next({})
, etc. You'll also notice that a lot of these only get called if certain conditions are true
. For instance, inside of the onChange handler provided by register
, there's a call to _subjects.state.next()
if the calling event is not a blur event
!isBlurEvent &&
_subjects.state.next({
name,
type: event.type,
values: cloneObject(_formValues),
});
That state
variable present within subjects
is an instance of a Subject
class, which exposes a next
method (like the one seen above) as well as a subscribe
method. It also keeps an array of subscribers under the _observers
variable. When a user calls subject.subscribe()
they are able to pass in an object with a callback under the next
key. The Subject
keeps an array of all the observers that are subscribed, and when a field is changed resulting in a change to subject.next()
, we loop over all valid observers and trigger their callback. If no observers are present, the next
function does nothing.
/** slightly simplified version of the actual thing in RHF */
const Subject = () => {
let observers = [];
const subscribe = (observer) => {
observers.push(observer)
}
const next = (data) => {
observers.forEach(observer => observer.next(data))
}
return {
get observers() {
return observers
},
next,
subscribe,
}
}
The useWatch hook makes use of this observer functionally in order to "listen" for when the state should be updated for the specific field we are watching. Internally, useWatch sets up a value
state variable and an updateValue
setter function. On mount, useWatch
subscribes to updates from the subject
, and the observer that gets passed in contains a callback that updates the value
state variable only when the name
of the field being updated matches the name
of the field being watched. It looks roughly like what we have below.
const useWatch = (name) => {
const [value, updateValue] = useState();
// Get the form control object from the overarching RHF context.
const { control } = useFormContext();
useEffect(() => {
const observer = {
next: ({fieldName, newValue}) => {
if (fieldName === name) {
updateValue(newValue);
}
},
};
// _subscribe is a wrapper around `subject.subscribe`
control._subscribe(observer);
return () => {
control._unsubscribe(observer);
};
}, []);
return value;
};
This is a very simplified version of what actually happens in RHF, but it should give you a good idea of some of the cool stuff RHF does under the hood to avoid unnecessary re-renders. Most importantly, going into the source code and looking at how others employ the library is what made me understand why React Hook Form was the final victor of the Great Form Control Library Wars of the early 2020s. Very nice 😎👍
Here's a really cute diagram showing what we discussed above. Please be nice to it, I suck at diagrams bro 💔

Thanks for reading this far! I hope you enjoyed this blog post and maybe even learned something new from it. If you have any questions or comments, feel free to reach out to me on LinkedIn or Insta
What I'm BUMPIN Today
Last blog post was like a year ago so here's a couple of songs I've been listening to.