As mentioned in my React Hook Form 6 month update, I alluded to needing to implement our own, custom hook in order to accomplish our needs. At a high level, what was lacking from React Hook Form was as follows:
- A clean way of updating the Redux store with the resulting changes
- Immediate access to casted values
- Formatting capability
- Encapsulation of common input attributes
All told, it took about 100 lines of code to add everything we needed for the entirety of our system. Due to the fact that our source code is not open sourced, I will speak to what we did in generic terms. Apologies for that!
Our function signature is pretty simplistic. It’s comprised of two parts. The first is our React Hook Form configuration object that mirrors the same API defined in the documentation. We provide sane defaults to provide a consistent form experience across the application. This means defining the
The second and last argument defines our custom functionality. Right now, it consists of a formatters map, fields representing the Redux store, updateFields callback, and setCastedValues callback.
Note on the formatters map: we’ve defined this to consist of a flattened object similar to how RHF registers inputs via
name and the
register function. This allows us for easy access to the appropriate formatter.
All arguments & properties within said arguments are optional, allowing the consumer to opt into the additional features of our form hook.
Encapsulation of common input attributes
We have an
RHFInput component that encapsulates our Label, Input, how we display our ErrorMessage component, etc. Everything that’s needed to integrate an
RHFInput should also be encapsulated, right? RHF requires, at a minimum, a
ref attribute, but we also want to set our
errors, and event handlers onto the field in a consistent way.
In addition, as you’ll see in the next few paragraphs, we need to effectively wrap our user input change event handlers to perform formatting, casting, and Redux related callbacks. These event handlers are simply a pass-through to our custom
This also applies to checkbox input props, which, instead of value, we use
defaultChecked and all events are on change.
Immediate access to casted values
Yup, if you’re not familiar, is a declarative validation library that gives us the ability to not only validate user inputs, but use those same validation chains to transform data values to whatever data we need. Our main usage of this is to validate user inputs from text elements (strings) to numbers.
React Hook Form uses this functionality to cast values when you submit the form, but we need to access those data values to perform down stream programmatic updates to other values. For instance, when one changes the “down payment” field, we need to update the amount on the loan, as well as the “loan to value” ratio. This means needing access to the number value underlying the user’s string entry.
At a high level, this means that any
setValue call, whether through user input or from other means, we need to run it through the Yup schema’s cast functionality. Unfortunately, when this is done and a field fails validation, it throws an error. Our approach to handle this is to keep track of the result of such a cast. In the event of it succeeding, we update a React ref with both the resulting casted values and the form state returned by
getValues . The failed casts will result in
errors value of React Hook Form to reflect the validation failure. We use that object to determine which values led to errors. We replace whatever value led to that error with the last successful value at that same property. Thus, we will have a successful cast when the user addresses all errors in the form.
When we do succeed, we call a callback to the consumer, allowing them to re-render using the casted values and respond accordingly.
Updating the Redux store
This one is pretty simple, given the last section! We take special precautions to ensure that all updates to the form state result in casted values, right? All we have to do is piggy back on those same functions. When we perform either a
setValue , we update the casted values. Those are ultimately what we want in our Redux store, as well. When we perform the cast, we update the store. Easy peasy.
Formatting user inputs
Similar to the above cases, formatting user inputs is accomplished by performing the formatting at any point in the data lifecycle. This occurs in a few different fashions, either by deeply formatting objects according to a formatter map, or individual fields. In each of these cases, the formatting itself is performed by executing a formatter function for a given value. The result of said function is then passed along to the underlying React Hook Form methods that were intended to be performed (i.e. a
setValue will call RHF’s
Deeply formatting the values occurs when we initialize the React Hook Form with
defaultValues , when
reset is called, and also when a
setValue is called with an object. In all of these cases, we iterate over the keys of the object representing the new form fields, and format them accordingly.
Individual fields are formatted against the same formatter object, but for a singal
key. This occurs when a user inputs a value (onChange/onBlur), or when a key/value pair get passed into
setValue as opposed to an object.
If a field does not have a corresponding formatter, no changes are needed!
I have found that very little is needed that RHF does not provide out of the box. Our simple hook gives us all the leverage needed to take full advantage of React Hook Form’s functionality and meet all of our business needs. In addition, it’s a simple hook which makes it easy to test. Given it’s the backbone of almost every page of our application, it’s important to ensure we don’t introduce regression bugs!