Yup, you should use React Hook Form
My biggest complaint about React is forms. Not because I think React took the wrong approach in how forms are implemented in React, but because it’s the most challenging problem in using React.
Many frameworks have their own solution to forms. AngularJS did this extremely well. Formatter pipelines, parser pipelines, validation built in, classes added and removed from elements based on status…it was great!
Until you had to do anything that didn’t fit the mold that AngularJS had in mind. It was an absolute pain to interact with forms if you needed to do anything special!
So React did what it does best, it left it up to us to implement. In fact, it’s exactly why I love React! However, it’s more challenging than how you feed data to props. Parsing data, formatting, local state, prop changes…all of these are challenges in React.
I ended up writing my own framework when it was crunch time, and the longer I worked with it, the more I realized it only worked because it fit how I thought about forms in React. It aligned with my views on AngularJS (formatter & parser pipelines).
I set out to find something that was more broadly accessible than what I had developed as our team had expanded.
I evaluated the options based on the following criteria:
1) Clarity — It should be very readable and have explicit declaration of validation
2) Fits well within our framework — Needs to be able to use our design system input elements, update redux on changes
3) Size impact — It should not bloat the size of our code
4) Few “gotchas”
5) Wide adoption by the community
6) Does not rely on the form element and can live anywhere (form elements can’t be nested effectively)
7) Compositional approach — ability to do validation on a lower level and then compose the form
Formik — https://jaredpalmer.com/formik/
I had used Formik in the past, to somewhat meager success. It was great for true forms but struggled on multi-step forms. They have since added “wizard” forms, but it doesn’t feel very intuitive.
In addition, it relies upon a form element & was challenging to update the redux store with much control.
It does, however, have the highest adoption of the 3 I evaluated at 638,961 weekly downloads from npm.
Redux Form — https://github.com/redux-form/redux-form
We’re redux, we’re react, it seemed like a decent option. Nope! Definitely not. Even in their readme they allude to it being entirely inappropriate as a solution. He and I are in agreement: you should not tie your rendering to your choice in data store.
React-final-form was nicer and did fit closely with my viewpoints with the ability to swap out the component rendered via render props. However, I did find it to be significantly more verbose than React Hook Form without the niceties of validationSchema (more on this later).
React Hook Form
React hook form was my final exploration and ended up being my choice. From here on out, we’ll talk about RHF and why I believe it’s a great solution.
- Pretty seamless implementation. I find it to be extremely clean with a single hook call abstracting away a ton of underlying set up.
- Configurable — The hook functionality is flexible for many different situations.
- Built in validation — RHF includes local validation, as well as the ability to pass in a validation schema (more on this later).
- We don’t need to use Yup for validation. You can choose to provide your own validation schema, or use Yup, their suggested library. Which, by the way, has over a million weekly downloads (React has 6 million).
- Ability to change output shape within the form itself.
- Performance! React hook form, utilizing hooks, demolishes the competition of Formik and Redux forms.
- PropTypes becomes much easier for defining your form components because you’re not accessing the props directly (RHF manages it for you)
- Must be used within a functional component (hook requirement).
- Hooks. They’re relatively new to React, but do make for clean code (in my opinion).
- Updating the redux store isn’t super seamless. You have to perform an “updateFields” callback on blur of the input
- Formatting the input string (i.e. adding commas into a number) needs to call setValue again (this is more of a “neutral” point because we already do this within our current form framework)
- Nested inputs with their own validation can be a bit hard. For instance, if you have two inputs that are tied together (for instance, a % of a whole), the schema and internal values need to be managed together and also pass validation, but the parent only cares about the value & an whether or not it’s an error
Just to note: While there are 5 cons, I would say 1, 2, 3 are minor. They are more preference than downsides.
In case you have not had exposure to Yup, it is a declarative validation schema that provides chainable functions with pretty staggering capabilities.
- Insanely readable.
- Prioritization of validation as a result of the chaining (i.e. number().required().positive()) will error if there is data that isn’t a number, error if undefined yet required, and if it’s a valid number that’s not positive, will error.
- Custom error messages per validation failure. These error messages are pretty slick, you can interpolate the value that failed, the values entered into the validation function (i.e. min(0), 0 can be interpolated).
- Casting! Using the same exact validations, you can cast an input into a usable output (i.e. converting strings to numbers)
- Dynamic logic chains — You can use when to interpolate values from other properties to change the schema. For instance, using if you entered a value and want to use that to be the maximum of another property, you can!
- Concatenation of schemas: concat. This allows for complex forms & wizards to be validated across different areas of the app, and then have a final verification as a whole concept.
- It’s an external dependency — They have different opinions, approaches, changes we’re not privy to. For instance `number` reports `1,234` as invalid because of the comma. The only way around this was to create a new validation type. I would have personally done a logical sanitation before passing it to parseFloat
- Hard to swap out of once Yup has been adopted at a full application level. That’s a lot of validation to rewrite.
Bundle size impact
Beyond the sheer dominance of React Hook Form’s rendering performance, it blows its peers out of the water as it relates to bundle size. As you can see in Bundlephobia’s analysis, the only export from RHF is itself, confirmed by looking at its dependencies. This aids in keeping the bundle size extremely small.
React hook form adoption
React hook form has 8% of Formik and only 13% of Redux-Form weekly downloads. While this is a pretty glaring problem, hooks are new and broad adoption of a new hook based approach to forms will likely be an uphill battle. I do, however, think that the trade offs are well worth it.
I have used both before Formik and Redux-Form in a professional setting and have found react-hook-form to be leaps and bounds above either (to be fair, those were also used over a year ago, improvements may have happened since then to alleviate the problems I faced).
How I see us using this
The high level approach:
- Schemas live in a separate package (we utilize Lerna, so this equates to an npm module)
- Schemas are imported into the forms, and used in the `useForm` hook
- Updates to the store are performed on each change, updating the store with values/errors
- Using concat, we can perform complex validation across the entire “wizard forms”
Schemas living in a separate package
Yup schemas fulfill several roles as it pertains to the application data. Not only is it performing validation for the form itself (in the React hook form hook configuration), but also to cast data into a usable form in the reducer to then perform manipulation/calculations upon. This can be done by utilizing Yup’s
cast call on a schema for a given data object.
Beyond that, there are two types of schema which are extremely coupled, which is the form validation & also API validation. For instance, it’s possible that we want to keep all of the user inputs as it pertains to a dynamic form (i.e. when you are paid hourly, how much per hour & hours worked matter, but not when you are salaried). In the case of the API validation, we don’t want to send hourly information to the API, so we can utilize
strip within Yup to remove the data from the object.
Updates to the store are performed when hooks callbacks are fired
Most forms should be performing updates onBlur instead of onChange, but either way the callback should consist of 3 steps:
1) Use the getValues callback to pull data from the hook
2) Update the redux store with the values & any errors that may have occurred. We can choose to update the store values with the values that created the errors or not, either in the component or in the reducer.
3) Set the local value in redux-form-hook to be the formatted string of the input
Complex, multi-step, validation
Given the ability to do concat with schemas, and also branching validation, we can utilize this to do validation before even hitting the API.
Both libraries, Yup and react-hook-form are pretty solid implementations of each problem they are trying to solve. If anything can be taken away from the above is that either are able to stand on their own with flying colors. Combined, they make for a spectacular development experience.