React Hook Form for React Native
When looking for a way to integrate forms in one of my more recent React Native projects, I found React Hook Form after it being recommended by my colleagues at Echobind who used it as a solution for React web. It trades itself as a “Performant, flexible, and extensible [solution for] forms with easy-to-use validation”. I was excited to find its support for React Native, but as I dove into trying it as a solution, I was quickly confused by its lack of documentation and examples for React Native.
One of the concepts in React Hook Form is the ability to register your uncontrolled component into the hook. The current example utilizes a Controller pattern, wrapping all components on the same level. While this approach is straightforward for simpler applications, it can be cumbersome/problematic for larger apps because it doesn’t enable flexibility for nested component structures. There is a solution to this in the form of hooks, but the documentation is only limited to a React context.
This blog post will help guide utilizing React Hook Form to register form inputs on the component level via hooks, making its value available for form validation and submission for its parent components. When I started using React Hook Form for React Native, I based my learning on this article by Daniel Koprowski. Since then I’ve adapted it to using v7 and Typescript, that’s what this guide will be based on. So let’s get started!
Getting started
Assuming you have your React Native environment setup and dependencies installed, let’s start by adding the following code to match React Hook Form’s React Native example (https://react-hook-form.com/get-started/#ReactNative):
While in smaller apps, this pattern could work out just fine, but for more complex applications with multiple or extensive forms, custom/dynamic inputs, etc, this pattern can be cumbersome if a controller needs to be assigned to every TextInput component. So let’s make this more efficient by integrating the controller functionality within a custom TextInput component.
Setting up the files
1. Create TextInput component
The first thing we will do is create a custom TextInput component. Right now this component will be pretty basic. It will be a wrapper for the React Native Core TextInput component, with some styling (originally defined in App.tsx
) and props such as label
and any props inherited from TextInput
(renamed RNTextInput since we'll have our new TextInput declared) as inputProps
. You might be wondering why we aren't passing props like value
, onBlur
, andonChangeText
? Don't worry we will get to that part later.
2. Integrating React Hook Form into our TextInput component
This is where the magic begins. There are two main hooks that we will want to import from React Hook Form, useController
, and useFormContext
. useController
hook establishes the instance of our controlled input and stores its value to the form, and the useFormContext
hook will allow us to access the form's context, its methods, and state.
For useController
to work, name
is required to be registered to it. Optionally, you can also pass along any validationrules
, and an inputs defaultValue
. So let's add those as well to our TextInput props and register them to the controller. Be sure to also extend the TextInput props to inherit the UseControllerProps
.
Again, without a name
, the app will throw an error. We can safely type our component to require it. Additionally, we could extend this even further by not rendering the input if name
or formContext
doesn't exist. I'm generally not a fan of this approach by itself, as it could not render all the inputs in the form and lead to confusing form submission errors if required inputs didn't render. So let's pass a message to the developer to inform when the formContext
or name
does not exist.
Now there’s one more problem with this code. useController
is being called conditionally, breaking React's rule of hooks. So if we were to go with this method, we need to split out the logic to separate components. Let's relocate our useController
hook and rendered components and their props in a new functional component calledControlledInput
.
Now, this is accessible to our TextInput component and only returns whenformContext
and name
exist.
3. Hook up controlled state to our rendered component
The last thing we need to do for our input is connected it to the controller. We’re going to destructure the field
object returned by the useController
, and assign the root component value
, onBlur
, and onChange
props with these field properties, giving react-hook-form
full access to our component.
4. Import the TextInput component into App.tsx
Now that our custom TextInput component is controlled by react-hook-form
, let's go ahead and replace the Controlled
component with our new controlled TextInput
component inside App.tsx
. To work properly, we will need to wrap our TextInputs with the FormProvider
. We will need to pass all methods into the context by passing the methods from useForm
to the FormProvider component. Now our TextInputs will be fully registered and controlled by react-hook-form
. Our submit button will live outside of the FormProvider, but can receive the form data by passing handleSubmit
with its success and error handlers.
There you have it, we now have an extensible form that can scale and is much cleaner than the original example. This component makes it easy to add extra features like inline validation and error handling. To see a full example of this, you can find it here :
Alexandra (Alex) is a Senior Software Engineer at Echobind, who is passionate about clean, friendly UI and reusable code. Alex is a happy cat mom, who when not coding is an aspiring artist, runner, yogi, and VR enthusiast. She also loves to explore the PNW where she calls home.