React Hook Form, 6 months later

React Hook Form logo

At the time I wrote my justification of React Hook Form, our adoption of it across the site was pretty low. Our scenarios were rather trivial and we’ve had a chance to expand React Hook Form (RHF) across several new use cases (1 implementation to 6, inclusive of API interactions). Now that we’ve got more under our belt, it seems worthwhile to post a follow up with the issues we ran into, and how we addressed them within our RHF framework.

The original intent of adopting RHF was to make React more approachable to our broader team, which consists mostly of full-stack developers.

Programmatic updates to RHF (Formatting, Redux)

Defining the source of truth: Redux or Form State?

Given that we have two states that represent the same data set, Redux and RHF, we need to evaluate who is the source of truth.

General knowledge states that Redux is the source of truth for application state. The broader application is driven by what is in the Redux store, inclusive of the starting state of our forms. When we access a page, we fetch information from APIs that goes into the store, selectors feed it to the component, the component feeds it to the useForm hook.

Now we have two copies of the data where an update could occur in a number of different ways: React components programmatically setting values, user inputs, API calls resolving, etc. This feels cut and dry when you have a clearly defined lifecycle of form data, such as a submit button that defines a point in time in which data goes from user input to application state.

However, our use cases involve down stream updates based on form inputs from the user, as well as relying upon Redux to persist data through refreshes (we persist user inputs into local/session storage on Redux action). This means if we want a seamless user experience, we need to update the Redux store significantly more often than simply when a user submits data.

As a result, we’ve built a custom hook that wraps useForm to define when, and with what data, Redux is updated. For details, I created this blog post highlighting our approach.

Formatting user inputs

Our business need is to format input fields to be well formatted both when the values are loaded from the API, but also on blur of input fields. This both gives us a better user experience than trying to format as you type (we all know the challenges there!), as well as a clearly defined event to perform the formatting on. This also makes the problem a lot simpler, as we know the user has completed entering their intended input.

We are now several levels separated from the underlying input reference. We have our own RHFInput element, which is a wrapper around our design system Input, a styled component, which is an input element underneath it all. Defining a way to format user inputs in a purely visual way is challenging. Solutions like react-input-mask comes close in terms of the user experience. However, passing all of the properties through yet another dependency to an underlying styled system and also ensuring that eventing works as expected was a challenge. In addition, it falls short of our needs. For instance, a user entering 1000 should result in 1,000, but masks are too rigidly defined to support all of our use cases. Our inputs’ formats can’t fully be known for each field to define a fixed mask pattern.

In the past, our inputs worked similar to how AngularJS inputs worked. There’s a parser array and a formatter array. As a user inputs data, it evaluates both. The result of the parsing array is set to the ngModel value (in our case, what is sent to the onChange or onBlur callbacks). The result of the formatter array is set back to the input value attribute. This results in useful data for the application, and a value that reflects the formatted result of the user’s keystrokes. This requires the parsers array to be able to consume both the raw user data, as well as the formatted user input.

Our React Hook Form solution works in a similar way, instead of relying on a dummy input and event forwarding like react-input-mask does. However, this requires us to encapsulate formatting & parsing in each update to the RHF form state. Thankfully, this was pretty easy as a result of having a useForm hook wrapper.

Continued improvements, docs, feedback

Force Multiplication

One of the biggest benefits of using a third party library is the force multiplier it provides to a project. I am one developer. I like to think of myself as hard working, knowledgeable in the problem space, and capable. However, there are only 24 hours in a day and a backlog a mile and a half long.

However, I have, according to github, 97 software engineers working on my form framework! Since adoption, 80 releases have been made, including three major releases. This is inclusive of bug fixes, but also new features like the ability to manage collections of inputs, dev tools, an ErrorMessage component, and API improvements.

Also, as a result of supporting a broader community (adopted at ~31k downloads/wk, it’s now at ~251k downloads/wk!), it is a more robust system than anything I can build myself.

Caveat: I am still one who prefers to write my own implementations, but can admit there are times in which this gets in the way of progress. Make sure to analyze your dependencies and make sure it is worth adopting before proceeding! That’s the intent of this article :)

Documentation

Beyond a shadow of a doubt, React Hook Form has among the best documentation sites out there. This is aided by the fact that the system itself is simplistic, as well. You don’t need to have obscenely verbose documentation if your software is well constructed, focused, and easy to interact with.

In addition to the API documentation, the maintainers also include substantial documentation in terms of advanced usages, how to work with RHF in other contexts (such as pairing it with Material), how to build common form based features like wizards, has a code examples repo, FAQ, and a resource page filled with blogs (hint hint).

Maintainers & Github

First off, let me give a huge shout out to Bill for being among the best maintainers I’ve interacted with. He has always been extremely responsive to github issues and does his best to grock issues & help provide direction. In addition, he is very accessible on multiple platforms, which has been an awesome consumer experience for me.

As alluded to earlier, having Bill & team maintaining RHF has been a huge productivity boost for myself at a minimum. They’re doing this all at no charge to me or my company. They are fielding questions on a public forum for all to read. This has allowed for me to find solutions to issues I was encountering without needing to wait for a response. Beyond the force multiplication of the maintainers themselves is the additional support from the community in terms of experimentation and questions.

Challenges faced

In order to form a well informed opinion, you should also know the downsides. Here are some of the issues that arose in the same time span.

Learning curve

The learning curve is a bit two-fold. On one hand, it was a forcing factor for me to learn more about React hooks, which I was slow to adopt because they were a pretty large departure from what I was used to (let others experiment, adopt when stable).

The other learning curve is how React Hook Form itself functions. It is understanding the core motivations of the system, the data lifecycle, and the conscious technical decisions and how they affect you. For instance, understanding useFieldArray was a bit of an uphill battle because fields is just the objects used to track the references and not the actual input results (those are accessible via getValues like any other field).

Once you gain familiarity, things move much faster! Unfortunately, with a rotating cast of developers on a project, learning the nuances of a framework in a narrow scope is challenging.

Interdependent field updates

Admission of guilt: this was a challenge for me mostly because of my “newness” to React hooks and not an RHF problem. Paired with the knowledge I garnered while expanding our hook wrapper, the underlying root cause was my lack of hook exposure.

I was creating a casted object which was the result of the Yup schema cast, but for some reason it was always the wrong value and one render loop behind…turns out, it’s because I wasn’t forcing a re-evaluation of the hook. Explicitly calling a state setter with the casted values results in everything I need to use useEffect hooks to update other values.

We’ve considered implementing a sideEffect map to say “execute this callback on change of this field” type behavior. However, with basic understanding of hooks, the need for this is alleviated.

Sub-forms

This is definitely of the more challenging aspects. We have inputs that we want to encapsulate certain expectations. This means having useEffect hooks that directly respond to changes. One such example is downPayment which is a factor of the home price, a down payment dollar amount, and the percentage of said home price. We want to ensure the $ matches the % of the home price, and all of the corresponding changes between those 3 values. We have to pass getInputProps down into the component, and the validation schema needs to be declared in the parent form. There’s no clear way to define the holistic behaviors of the input, such as the interdependence of the fields as well as the validation, since it is required to be used in the context of a form hook call.

It’s not my code!

Part of adopting a third party library is the fact that you don’t control it. You can be an active contributor or commenter, but ultimately if you do not have master merge rights, you have little control. This is one of the downsides of open source. You have to trust the skills & direction of the maintainers of the packages you rely heavily on.

Decisions are made based on the context of the primary drivers of the maintainers, not the context of your application. A decision to no longer support IE 11, for instance, would leave us dead in the water. Don’t have the ability to use custom input elements? Broken.

Thankfully, we have not run into a case of this with React Hook Form. At worst, I have disagreements about function signatures being overloaded (`setValue` has two function signatures, function(key: string, value: any, shouldValidate: bool) and function(set: Record, shouldValidate: bool) . This is a code smell to me and permeates through to our wrapping hook.

More importantly, I have to work around the system if what we aim to do does not easily fit into the broader system.

“Around the edges” use cases

Much like any codebase, you are forced to make decisions about how you construct your code to solve a particular problem. You evaluate the pros/cons of where particular actions take place, evaluate if something is truly re-usable or not, performance vs readability, etc.

When it comes to interacting with React Hook Form, the majority of cases are cut and dry. You call the hook, you register your inputs, you provide a submit handler, and you’re done.

However, what happens when you need real time updates to your Redux store? Well, you can do a useEffect hook to evaluate changes to the values, not very reusable. watch is another option, but fires on every change as opposed to onBlur. I could add an onBlur attribute to every input, but that’s additional effort to either have the store accept single field updates, or having to use getValues to propagate the changes to the store. Also: I want the underlying data as casted by Yup, like it is when onSubmit is triggered in order to do math on the values.

All of the above use cases has led us to create our own form hook. Follow up on that to come. If these changes were purely supplemental changes, then I would be extremely happy with React Hook Form. However, I had to override setValue and reset to fit our use case. The choice was between creating a new API to consume (such as resetWithFormatting, and setValueWithFormattingAndCasting), or mirror the API that the documentation sets forth. For the sake of usability, we overrode setValue with a custom implementation.

This was by and large the biggest challenge. At the end of the day, it solves all of our needs, aligns with the public interface, and is entirely encapsulated in a single hook. It is fully tested, because hooks are functions and it makes it easy. All in all, ~100 lines of well documented modifications seems like a worthwhile exchange for the functionality we have received from RHF.

Conclusion

I started writing this 2 months ago when we hit our peak (4 front end devs, 5 full stack devs) in the front end. We went from flat objects with 100% of the formatting, validation, and casting being done inside the singular form to 6 forms with deeply nested objects and complex form field interactions & validation chains. We added API interactions both in establishing form default values as well as submitting to services.

All of this has been accomplished mostly with RHF out of the box, and ~100 lines of code in supplemental functionality. It has greatly increased our productivity across multiple experience levels.

While there were some tricky issues encountered, they were solved with just a little extra focused effort by myself and we now have a pattern in place for next time.

Here’s to the next 6 months!

Front End Developer