How to create reusable form components with React Hook Forms and Typescript
Why Should I Create Reusable react-hook-form Input Components?
Like many in the React community, you've decided to use react-hook-form. While not every situation calls for creating wrapper components around react-hook-form, there are some situations where doing exactly that may be ideal. One such example might be creating a reusable component library for your organization. You might want to reuse the same validation logic and error styling in multiple projects. Maybe your project contains many large forms and in this case, reusable react-hook-form components can save you a lot of time. If you've decided reusable react-hook-form components are what's right for you and your team, it can be hard to understand how to make these components especially if you and/or your team have also decided to use Typescript.
Step 1: Create An Input Component
The first step is to create an input component. Creating an isolated component can be a good way to provide consumers of your component with a way to use inputs that aren't directly tied to validation or react-hook-form. It can also help consolidate styles and isolate input logic for easier unit testing. You can also use this component to enforce good accessibility practices by ensuring that inputs have a label, type, and name.
Step 2: Creating a Form
Before we move forward, we need a form. Let's imagine a registration form.
`
Eventually, we'll want to make the first name field in this form a required field and display an error if that field is not provided, but before we can add any validation and see errors, we'll need to add a way to submit the form.
In our , let's destructure the handleSubmit function off of our call to useForm, then use that to create an onSubmit function we'll call for our form submission:
`
We should have something that looks like this:
Step 3: Create a Validated Input Component
We want create a wrapper component that uses both our component and react-hook-form to create a reusable component that can be passed any validation rules and any potential errors. Let's make a new component called .
`
Registering Our Input with react-hook-form
Next, we need to register our input with react-hook-form. Errors and rules won't work until we do this. It may seem like our first step is to pass a register property given to us by react-hook-form to our generic component. When we attempt to do this, we'll encounter an issue. When defining our register property, what type should we assign to register?
`
Let's take a look at what react-hook-form says is the type of register:
We can see that the type of register is UserFormRegister, but passing in a register property of type UserFormRegister wouldn't help us keep our component generic. Not every form will have inputs that belong to our registration form. Each form will likely have different validation rules and errors. Now what?
The answer lies in generics. We need our to have a register property that can take in _any_ type of form. Let's change the type for our 's properties.
`
is a generic type that represents any potential form. In this case, our form type is RegistrationFormFields, but using means that this component can be used in _any_ form. Next, let's take a look at how we can pass the register property to our input.
`
Now, we can register an input used in a form of any type with react-hook-form. We aren't quite done with register yet, however. We still need to tell react-hook-form _which_ input/field in the form we are trying to register. We'll need to do this with a name property that we will pass into the register function.
Defining the Name Property
It may seem like our new name property should be a string, because it represents the name of one of the fields in our form. Upon close inspection, we can see that the type of parameter register is expecting is a Path to one of the fields in our TFormValues.
The reason name is a Path is because our form type, TFormValues could potentially have nested fields, like objects, or arrays. For example, imagine that for some reason, instead of registering just one person, we had a list of people to register. Our RegistrationFormFields might look something like this instead:
`
If the name property of our input field was just a string, we couldn't reference the name of the first or second person in our form. When name is of type Path, however we can access the fields of any nested object or array. For example, name could be people[0].firstName or people.[1].firstName.
The properties for our component should now look like:
`
But wait! Now both InputProps and our FormInputProps have a name. These names are _not_ the same thing either! The name in InputProps represents the native name on an HTML input element. The name in FormInputProps is a Path to the field in our form. We still want to be able to pass other native HTML attributes to our input. We'd just like to exclude name from InputProps because we are defining name as a Path FormInputProps`.
`
The last thing we need to register our input with our react-hook form is validation rules. We will need to pass our validation rules to this , and then into the register function.
Defining the Rules Property
Remember, we want to be able to pass in any rules to our component, not just rules that apply to our registration form. Thankfully, this one is a bit easier.
`
Let's use these properties to actually register, apply rules, and render our component. We also want to replace the component we were using with our new component in our . We should have something that looks like this:
Sweet! We're still missing something though. No errors are actually displayed when we don't enter a first name. Let's fix that.
Defining the Errors Property
When defining our errors property, what type should we assign to errors? Let's take a look at what react-hook-form says the type of errors is.
We can see that the type of errors is:
`
Like before, passing in an errors property of type { firstName?: FieldError; } wouldn't help us keep our component generic. Not every form will have errors for a registration form. Each form will likely have errors. We'll have to use our generic again:
`
You might be wondering how we could conclude that the type of errors should be DeepMap. This one was a bit tricky, but eventually we can find the type by looking for FieldErrors in the TS documentation for react-hook-form.
Let's pass the errors we're getting from react-hook-form in our into the errors property of the .
Looks like the errors type we are passing in from useForm does _not_ match the errors type we are expecting in our .
The errors type we are passing in from useForm has a type that looks like this:
The errors property we are expecting in our looks like this:
`
It looks like our errors property on our is of type DeepMap. It requires that either errors are completely empty, errors?, or that every property in our TFormValues exists on our errors. This isn't quite accurate.
There won't always be an error for every single field in our form. Our errors will dynamically change as the user inputs data. There _might_ be an error for the firstName field, but if the firstName field is valid, there won't be an error for it. So although our RegistrationFormFields says that firstName is _not_ optional, our errors must declare that any firstName errors _are_ optional.
We can achieve this by making our errors property on our a .
`
This means that the errors don't have to include all of the fields in our TFormValues, and with that our errors Typescript error is now gone!
Displaying Errors
To make sure that our error styling is consolidated, and the same across our inputs, we'll want to put the code to display these errors in our component.
Remember, errors contains _all_ the errors for our form, not just the errors for one field. To render our errors, we'll want the get the errors relevant to this specific input. Your first instinct might be to get the errors by the name of our input, like so:
`
However, name is a Path. It could reference a field on a nested object or in an array. For example, we'd need to get errors.people[0].firstName and not errors['people[0].firstName'].
To do this, we can make use of lodash.get to retrieve the value of errors via a Path;
`
We're still missing something, though. Currently, errorMessages is an object representing each violated rule for our input.
For example, let's imagine that we had an email field in addition to our firstName field. This email field should not only be required, but should have a minimum of 4 chars entered, and match a valid email format.
If more than one rule was violated, like in the case that the email entered was both too short and didn't match the email format, our errorMessages would have multiple keys called pattern and minLength. Pattern would indicate that the email format was not valid. minLength would indicate that the email entered was not long enough.
However, most of the time, we only want to display only error message at a time. But how do we know which error message to display in an generic input component? Every form's type will be structured completely differently. We don't know what the shape of errors will be. How can we display specific error messages when the type could look like anything?
Fortunately, there's another package we can use that contains a component that can do this for us. That component is called ErrorMessage, and it is provided by @hookform/error-message. Using yarn or npm, install this package.
npm install @hookform/error-message
or
yarn add @hookform/error-message
Once that package is added, we can use it in our :
`
The component takes in an errors property. This is the same as the errors we passed into our , and represents all errors for all fields in the form.
The second property we are passing into is name. The type of name on does not match the name property we are taking into . For some reason, the name is of type FieldName>> on the component, but is of type Path in the register call, even though both name properties represent are both the Path of a field in the form. Because the FieldValuesFromFieldErrors type isn't exported, I couldn't cast name to FieldName>> and therefore had to cast it to any.
The last property we are passing into is a render function. This render function gives you access to the single error message you'd like to display. You can use this function to display the message in any way you'd like.
Conclusion
You now know a bit more about extracting validation logic from react-hook-forms to create reusable components in Typescript. You can continue to use this pattern to create more form components like checkboxes, phone number inputs, select boxes and more. In my next blog, I'll cover how to use a different schema validation resolver, like yup, and how to display server side validation errors using the same components. Take a look at the demo below to see a closer to real life example.
Live Demo...