Another ReactJS form library comparison

David Epely -

First of all, the purpose of this blogpost is not to tell you which form library you should choose, because you may not use React or you may not even be developing an application, but you may still be interested in our process. This is the actual goal, to provide you with some key points or a guide to use for your own processes. We would especially love for you to share your own experiments afterward.

So the main technical issue is to not jump on the first popular solution. According to the criticality of the part of the application, it requires some investment of time. Which brings us to the first point: the context. The second issue is to know what result to produce following the comparison. We chose to write an internal wiki page for the front-end team… and we thought it would be nice to share this report with you.

Context

Apps at Gandi historically use redux-form since it was a very common choice at the time and is based on redux like the rest of the apps. But the library has been deprecated since then and maintenance has almost stopped and we never use its redux capacities outside of implemented forms.

Some time ago we created an internal gandi.react-form library which provides abstract components and connected components using redux-form.

Several other forms have been implemented, sometimes using redux-form, a home made form, custom utility or no library at all depending of the needs of the form.

So for complex forms we need a replacement solution where redux-form is used.

This comparison is based on a curated list of form libraries (Looking for the Best React Form Library? It’s Probably on This List) and some other.

We also need to consider that the library can be migrated to a mainstream framework and should be compatible with the current best practices of the JS environment.

And last but not least, the library should meet our needs of static type checking. This drastically improves the developer experience and the code quality compared to vanilla JS. We are using Flow but it would have been the same if we were using typescript.

Libraries

Fast comparison

First we dropped KendoReact Form that does not fit with our love for open-source and knowing that a 3rd party may have control over the features we provide is led us to rule it out.

library size1 nb deps stars remark
formik 13 kB 7 31k referenced by react; support yup validation schema
react-hook-form 8.9 kB 0 30k
react-final-form 3.3 kB + 5.5kB 1 7k rebranded major version of redux-form without redux
formsy 7.2 kB 3 751
react-form 4.5 kB 0 2k same dev as tanstack-query but seems to be left behind

It might be interesting to consider using a validation tool:

  • yup 18.2 kB | 7 deps | 18k stars
  • zod 11kB | 0 deps | 12k stars
  • joi 42.3kB | 0 deps | 19k stars
  • vest 8.3kB | 4 deps | 2k stars

Detailed comparison

First thought, it’s not because the library is popular that we should use it, it’s just a good hint that the library is actively maintained and reliable. It’s not because the library is light-weight that we should use it either; the features provided need to be compared.

However, time is also a limitation and so we must make our decisions efficiently. We will therefore limit our candidates to the first three competitors (formik, react-hook-form and react-final-form) mostly because of their popularity.

Questions

  • Can the library be used with the current state of our form components?
  • Does the usage of the library fit our needs: (SSR, validation sync & async, global & detailed errors, wizard, composed form, …)?
  • Is the migration of a form “easy”?
  • How verbose is the implementation?
  • Is it “fast” out of the box?

So for each library we will try to implement it on an existing simple form and answer these questions. I have selected the form to edit a “linked zone” on the domain administration panel for our experiments. The form is pretty simple: 1 text field, 1 select with async loading, 2 radiobox and 1 textarea. Also it is based on redux-form and it has initial values that may be loaded on render.

Formik

pros:

  • Flow typing is provided by flow-typed
  • Documentation: good
  • similar to redux-form: field needs a similar implementation:
    - import { Field, type FieldProps } from 'redux-form'
    + import { Field, type FieldProps } from 'formik'
    
    - function CustomField ({ input }: FieldProps) {
    -   const { value, ...inputProps } = input;
    + function CustomField ({ field, form, meta, ...rest }: FieldProps<string>) {
    +   const { value, ...inputProps } = field;
    }
    
    
    function MyForm {
    -  return <Field name="demo" component={CustomField} />
    +  return <Field name="demo">{(props) => <CustomField {...props}  />}</Field>
    }
    

Note: we cannot simply use component prop since meta is not provided cf. https://github.com/jaredpalmer/formik/blob/e677bea8181f40e6762fc7e7fb009122384500c6/packages/formik/src/Field.tsx#L203

  • Reduces boilerplate compared to redux-form (it looks less like a pasta bowl) just because it does not need any HoC
  • It’s way easier to get data outside of a field:
    -  // in this scenario we don't know the name of the form so we use a Context:
    -  const formName = React.useContext(FormNameContext)
    -  const { bar } = useSelector(state =>
    -    formValueSelector(formName, getFormState)(state, sectionName)
    -  );
    +  const [,meta] = useField(`${sectionName}.bar`)
    +  const bar = meta.value
    
  • In order to do programatic submit or retrieving meta data everywhere in the app, we can use the Formik props (<Formik>{(props) => …) or useFormikContext hook. Easy.

cons:

  • There is no substitute to FormSection that helps to split & reuse part of form, the benefit is the form mapping will be more descriptive and less prone to bugs but a little more verbose.
    - <FormSection name="foo" type="section">
    -   <Field name="bar" />
    + <section>
    +   <Field name="foo.bar" />
    
  • Every time a single key is pressed in the form, the entirety of the components within Formik are re-rendered, so it might not be the best we could expect especially for our CodeEditor which has syntax coloring & other features (14 re-renders for typing 6 characters).

This is the react profiler, I’ve typed some characters in an input field and recorded what happpens in the CodeEditor component which is a sibling. (Please do not pay attention to timers, since it’s in a dev environment, they can be a good hint but they’re definitely not realistic values.)

benchmark-codeeditor_formik_profiler

In React world, re-redendering is a common behavior, so it might sound awful but in fact what matters are reflows and repaints. Using the performance tool, here is the monitoring for the same key press, and as you can see, nothing to worry about:

benchmark-codeeditor_formik_perf

  • Beware of useFormik hook, it’s not meant for the majority of cases even if it sounds attractive.
  • CodeEditor which is a bit complex is not compatible out of box, some modifications need to be made for the onChange.

Conclusions:

Formik is a good candidate, performances out of the box are at least similar to the previous solution, it is well maintained and has a big audience which makes it move a bit slower (which is not bad) but it’s very reliable.

Also it’s very flexible and the API is well thought-out.

Migrating from redux-form, may not be the easiest part but the API is still pretty similar to what we had.

Bonus, schema validation using yup sounds really cool (I mean that it’s not verbose, easy to read, easy to learn and easy to implement).

FinalForm

pros:

  • There is a migration guide
  • Flow typing is provided by default
  • Documentation: good
  • Same API as redux-form for Field
  • API is very similar to Formik too
  • Every time a single key is pressed in the form, the entirety of the components within FinalForm are re-rendered. But the result is clear, for 6 characters only 8 re-renders (against 14 for Formik) out of the box, and performance can be improved even more. Also the performance looks much faster on the audit tool (but we still can’t be sure of this since conditions of execution may vary a lot). benchmark-codeeditor_final-form_profiler

cons:

  • FormSection has not been ported and needs to be rewritten as with migrating to Formik
  • Some weird typing errors can appear. Mostly when we use too many strict types
  • The maintenance of the library is not clear, it seems to be in slow motion currently

Conclusion

React-Final-Form is also a good candidate. The most important argument is the necessary time to migrate to this library; except import, none of these Field need changes. This is big. Also, since the API is similar to Formik, it could also be a simple step to reduce our techical debt.

Redux-Hook-Form

pros:

  • It sounds fast on paper thanks to ref & the register paradigm
  • Validation is integrated by default and it supports yup and others
  • A devtool is available
  • Performances are really cool out of the box, only 7 re-renders for 6 characters (and an input focus) and limited to the edited section: benchmark_react-hook-form_profiler

cons:

  • No Flowtype definition (but typescript definition looks awesome)
  • Implementation of forms is pretty far to what we have currently re-implement the form and the fields.
    // field
    const controller = useController({ name: 'nameAndOrga.name' });
    
    return <InputTextGroup {...controller} />
    
    // form
      const methods = useForm({
        defaultValues,
        resolver: validate,
      });
    
      <FormProvider {...methods}>
        <Form
          onSubmit={handleSubmit(onSubmit)}
          className="form-stacked"
        >
        
    
  • It seems, since we mostly use connected component and components from the design system that we have to connect all fields and it requires to pass control variable in all the component tree where it’s needed, it’s not explicitly documented but fortunately we can use FormProvider to prevent using a drilling pattern.
    function App () {
      
      const methods = useForm();
      return (
        <FormProvider {...methods}>
          <form onSubmit={methods.handleSubmit((data) => { /* do things */ })}
            <NameOrgaSection />
            
          </form>
        </FormProvider>
      )
    }
    
    function NameOrgaSection() {
      return (
        <>
          <InputTextGroup name="name" label="Name" />
        </>
      )
    }
    
    function InputTextGroup({ name, ...rest }) {
      // In this _simplified example_, this hook needs `control` prop or the `FormProvider` to work
      const controller = useController({ name });
    
      return <>
        <Label {...rest}/>
        <input {...controller} />
      </>
    }
    
  • depending on how we implement form validation, it will need some work to make it compatible: schema or field validation (rules prop)
    - const validate = (values) => ({
    + const resolver = (values) => ({
    -   name: !isDefined(values.name) ? 'Required' : undefined,
    +   name: !isDefined(values.name) ? {
    +     type: 'required',
    +     message: 'Required',
    +   } : undefined,
      })
    

Conclusions

Despite the astonishing results of performance in the usage, it may not be the best choice. The verbosity of the implementation is quite similar to other libs, it’s bit more complex and I found the documentation wasn’t quite advanced enough at least to cover our ecosystem. And, the final sword stroke, the lib will require way more refactoring than the others for any migrations or new features.

Final conclusions

I’ve limited the results to the first 3 main competitors. Most of the time the need for a “form framework” is for implementing complex forms, so we won’t reinvent the wheel and we need to choose wisely.

According to profiling, react-hook-form is “faster” (less re-rendering) and what’s more; limited to the closest child connected to the field, with 6 typed characters:

  • current implementation gives 16 re-renders
  • react-hook-form gives only 7
  • react-final-form gives 8 and has tools to optimize slow forms.

A performance audit gives a good hint but may not be reliable. However, by this metric react-hook-form is the winner.

Concerning migration complexity, React Final Form is easier & faster. Obviously, it is almost the same lib as before without redux.

For verbosity, implementation and documentation (new form or complex form), React Final Form is a good choice according to my experimentation. Unfortunately we can’t conclude that it will be the same for everyone (don’t get me wrong ;) ).

The size of React Final Form is the lowest with only 8.8kB (Note that yup or other schema validators could help in several cases to handle synced errors). The current implementation with redux-form takes 26.4kB which sounds huge in comparison and unfortunately we won’t be able to migrate all the implementation in one shot. With a deeper analysis we may find it could be easier than I think :fingers_crossed:).

Before starting this investigation, my first though was to use a popular library form like Formik or React Hook Form. But going through all of this (the migration path, the performance (downloading/rendering), the quality of the documentation and the experience of the author of the lib) I found that using React Final Form fits very well with our needs even if it isn’t the most popular. It seems to be just what we need, and it could do with more stars on github. Also we mustn’t forget what Erik Rasmussen said himself when he was asking how to pick a library: “Does the repo look well maintained?” and currently, final-form could do with some more contributions but we are confident that he will take care of it.


  1. (minified+gzip including deps) ↩︎