I've been working on an enterprise reporting tool for the past year. The frontend of that tool consists only of a couple of views and one of them is a relatively complex form, which is also the heart of this reporting tool.
Challenges
It consists of a lot of features that make the implementation of a form quite challenging. There are dropdown fields filled with items dynamically loaded from an API endpoint (1).
Other dropdowns show a list of items based on the selection of a dependent field (2).
The money input fields format the number based on the chosen locale while keeping an unformatted, pure JavaScript number in the state. Other money input fields depend on one or multiple money input fields and are sometimes calculated automatically (3,5). To make things harder, some calculations are bidirectional between multiple fields.
Imagine a price value and a commission percentage. Multiplying those values equals the value of the commission input. Should the change of the commission now change the percentage or the price value?
And to make things even more exciting, complete parts of the form are exchanged based on other fields (4).
Formik
It makes sense to choose a library to handle all the internals of form state handling, validation and so on not to reinvent the wheel. Formik is such a library: It has earned more than 28.5k stars on GitHub (link) and is even mentioned briefly in the React documentation (link).
Formik comes with a couple of appealing features: Support for a custom validation
mechanism, provides hooks to read from and update the state as well as a couple
of standard handlers for onChange
and onBlur
. It also has a component to deal
with lists and offers functions to deal with them accordingly (e.g. adding and
removing items).
With more than twenty fields with such special behaviors as described previously, each of which reacts to every key press, a form library is challenged not only in terms of its functionality, but also in terms of performance.
Performance
And indeed an input lag was noticeable which led to the discovery of Formik's
FastField
component (link):
<FastField />
is an optimized version of<Field />
meant to be used on large forms (~30+ fields) or when a field has very expensive validation requirements.
The problem is that every field update causes the whole form state to be rewritten.
This on the other hand triggers every field that is either rendered with the
<Field />
component or uses the useField()
hook.
This is calling for trouble and the sheer co-existence of <FastField />
raises
the question, why the other components are not optimized for performance from the
beginning on.
One a half years ago a GitHub issue was opened to raise awareness to the performance
issues that come with the useField()
hook, but it's not resolved. According to a
GitHub user commenting in this thread (link)
Formik's architecture makes it nearly impossible to implement an efficient solution.
Instead he proposes to rely on React.memo
to avoid unnecessary rerenders.
Unfortunately that's close to the solution we were forced to apply in order to
prevent those costly rerenders: Intead of wrapping the useField()
hook, which is
smart, we decided to wrap our custom input field components, like the money input
field and the dropdowns for example.
Conclusion
Formik offers a clean documentation, a great set of hooks and components to get the job done, and is a sufficient solution for smaller forms. Due to the serious performance bottlenecks in complex forms and the lack of improvement over the course of the past one and a half years, a choice in favor of Formik should be considered carefully.
React Final Form (link) is another library that offers "high performance subscription-based form state management":
For small forms, redrawing your entire form on every keypress is no problem. But when your form grows, performance can degrade.
No other form library allows such fine tuning to manage exactly which form elements get notified of form state changes.
Its API is quite similar to Formik in a lot of components, and therefore it's relatively easy to switch to this solution. There is even a migration guide available (link).
Additional features, like decorators (link), only add to its value and reduce the amount of code that needs to implemented manually otherwise.