⚙️
Post Cover Image
Formik and React Hook Form Beef explained
Loading Likes...

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

I started working with React relatively recently, joining the ecosystem around the same time that

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.

I hadn't played around with any other form management tools (including Formik) until after I joined ALTR. Since Formik's Hayday, the library has been receiving less frequent maintenance. Additionally, it seems to be on a downtrend in terms of monthly downloads. However, it still holds a significant footprint in enterprise applications and has an active community of users, as can by how active Formik's community issues and pull request boards continue to be! Today, I can say that I've used Formik just as much as I have RHF, and in terms of ease of use, the two really are not that different!

Today, we know that RHF is the most popular out of the two and is generally considered the industry standard. In fact, many experienced developers agree that RHF is the obvious choice over Formik. I always wondered, why is that the case 🤔. For the most part these two libraries are pretty similar. For instance both of them require the user to

call a hook

or

render a component

that sets up a

context

which serves as the initial entry point to the library. Users can then use

specialty functions

to bind inputs to that context, and pass a

callback function

triggered when the form is submitted. As I became more familiar with how state works in React, I realized why

RHF

no-diffed

Formik

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

The code is slightly different, but the functionality is exactly the same. If you mess around on the "Preview" tab of the sandbox you'll see that both forms work as expected.

However, if you click the

"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.

Now, if you add an author and type something into the first instance of the 'first name' field, we see that the

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, the

entire form

re-renders for each keystroke.
On the other hand, if you try out the same thing with the

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).
Now, I'm not gonna pretend this is some kind of heinous crime. A few more re-renders is totally fine on smaller forms not meant for heavy usecases. If all you need is a couple of static forms where the number of fields onscreen at any given time doesn't exceed 20 or so, then by all means go crazy. However, as your forms require more fields and complex render/validation logic, the performance impact becomes increasingly apparent. Unless you're very clever about how you memoize expensive components (with

React.memo

or if you use the

React Compiler

(yeah i watched that Theo video)) and/or using components like FastField, 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 the useFormik 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.