Another ReactJS form library comparison
»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
- formik (referenced by react https://reactjs.org/docs/forms.html#fully-fledged-solutions)
- react-hook-form
- final-form
KendoReact Formunder proprietary license- formsy-react
- react-form
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) => …
) oruseFormikContext
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.)
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:
- 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
forField
- 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).
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
& theregister
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:
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 useFormProvider
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.
-
(minified+gzip including deps) ↩︎