React Native Form Management Tutorial — Building a Credit Card Form

Halil Bilir
Jan 20 · 8 min read

A guide about creating declarative forms in React Native using react-hook-form by building credit card form example.

This story is originally published on my blog:

Forms are pretty common in all kinds of apps. That’s why developers are often trying to simplify the process of building forms. I’ve built some custom solutions before, also used all the popular form management libraries so far. I think react-hook-form is the best one in terms of developer experience and customization.

It’s pretty straightforward to use it on the web. You simply create your HTML input elements and register them. But it’s a little harder with React Native. So I’ll try describing each step I took to be able to make my approach more clear. I’ll be building a credit card form in this tutorial but the tutorial should be helpful with building any types of forms. Most of the components we’ll be building here can be reused as well.

You may find the full version of this component on Github. I also ported the React Native code into the web thanks to react-native-web. You may play with it on my blog.

The end result

Starting with a simple UI

For this tutorial, I used this clean design I found on Dribbble as the design reference. I’ve also used the TextField component I built in my last post. Here is the CreditCardForm component that generates the UI with simple local state variables:

I’m simply including the form in a ScrollView on the App component:

Integrating react-hook-form

Using react-hook-form provides subtle benefits over building form logics manually. The most obvious advantages are building more readable code, easier maintenance, and more reusability.

So let’s start by adding react-hook-form to our project:

npm install react-hook-form// oryarn add react-hook-form

You may use any TextInput component you have inside react-hook-form. It has a special Controller component that helps to register the input to the library.

This is the minimum code block needed to build a React Native form with react-hook-form:

While this is good enough for a single input, it’s a better idea to create a generic wrapper input component that handles repetitive work such as using the Controller and displaying the error message. For that purpose, I'm going to create FormTextField. It will need to access some of the properties that are returned from the useForm method. We may pass those values as a prop from CreditCardForm to FormTextField but that'd mean repeating the same prop for each input. Fortunately, react-hook-form provides the useFormContext method which lets you access all the form properties in deeper component levels.

And FormTextField will look like this:

Now, it’s time to migrate our form components to react-hook-form. We'll simply replace TextFields with our new FormTextField component, replace local state variables with a single form model, and wrap our form with FormProvider.

Note that it’s very easy to create Typescript types for our form. You’ll need to build a FormModel type that contains each field in your form. Notice that the field names should match the ones you're passing into FormTextField. The library will update the right field based on that prop.

After those changes, the new version of CreditCardForm will look like below. You may check out the full diff on Github.

Improving reusability

I had to make a decision at this point in terms of the better reusability of the form. It’s about where to create our form initially using the useForm method. We have two options:

  1. Defining the form inside CreditCardForm as the way it is. This makes sense if you'll use the credit card form in a single flow/screen. You don't have to redefine the form and pass it through FormProvider in multiple places this way.
  2. Defining the form in CreditCardForm's parent, which is the component that consumes it. You'll have access to all react-hook-form methods this way and you may build independent stuff upon everything CreditCardForm provides. Let's say you have two screens: one for paying for a product, and the other is just for registering a credit card. Buttons should look different in those cases.

Here is one example about the second option. In this example, we are watching the card number value changes and updating the button title based on that:

I’ll go with the second option.


react-hook-form lets us defining validations simply by passing rules to the Controller. Let's start by adding that to FormTextField:

For the tutorial, I’ll delegate the validations logic to Braintree’s card-validator library to keep us focused on the form part. Now I need to define rules for our FormTextField components. rules object will contain two properties:

  1. required: This takes a message that is displayed when the field is empty.
  2. validate.{custom_validation_name}: We may create a custom validation method here. I'm going to use it for validating the integrity of the input value using card-validation library.

Our input fields will need to look like below. You may check out the full diff of validation rules on Github.

After making those changes, we’ll see the following screen when clicking on the PAY button:

Triggering validations

The validation trigger scheme is configurable with react-hook-form without any custom code. mode parameter configures the validation trigger scheme:

onChange: Validation will trigger on the submit event and invalid inputs will attach onChange event listeners to re-validate them

onBlur: Validation will trigger on the blur event.

onTouched: Validation will trigger on the first blur event. After that, it will trigger on every change event.

While those modes are enough for most cases, I wanted a custom behavior with my form. I want to provide fast feedback to the user but it shouldn’t be too fast as well. This means I want to validate my input right after the user enters enough characters. That’s why I created an effect in FormTextField that watches the input value and triggers the validation when it passes a certain threshold(validationLength prop here).

Please note that this is not required for the form to function at all, and it may cost some performance penalty if your validation method is intensive.

Formatting input values

To make the card number and expiration input fields look good, I’ll format their values instantly with each new character users enter.

  • Credit card number: I’ll format its value in XXXX XXXX XXXX XXXX format.
  • Expiration date: I’ll format its value in MM/YY format.

There are some libraries that do a similar job but I want to create a simple solution on my own. So I created utils/formatters.ts file for this purpose:

Now we’ll simply create a formatter prop for FormTextField component, and pass the value it returns to onChange:

I created some tests to make sure format utilities return the expected values using jest’s test.each method. I hope it'll make it easier for you to understand what those utils methods are doing:

Focusing on the next field

I believe this is a good UX pattern for forms: focusing on the next input field when the user has filled the current input. There are two possible ways to understand when the user is done:

  1. Listening to the onSubmitEditing event of the input. This is invoked when users click on the return button of the keyboard.
  2. Checking the input validation results: it means the user has entered all the necessary characters for the credit card, expiration, and CVV fields whenever they are valid.

I’ll use the first method on the cardholder name input, and the second one on the rest. It’s simply because we don’t know when the cardholder’s name is completed, unlike other ones.

We need to keep refs for each input, and invoke nextTextInputRef.focus method appropriately. We have two custom components that wrap the React Native TextInput: they are FormTextField and TextField. So we have to use React.forwardRef to make sure ref is attached to the native TextInput.

Here are the steps I followed to build this:

  1. Wrapped FormTextField and TextField with React.forwardRef:
import { TextInput } from "react-native"// FormTextField.tsxconst FormTextField = React.forwardRef<TextInput, Props>((props, ref) => {// components/TextField.tsxconst TextField = React.forwardRef<TextInput, Props>((props, ref) => {

2. Created onValid prop on FormTextField component, and modified the effect that triggers validation:

3. Created a ref for each component and triggered the next input ref’s onFocus method:

You can check out the full diff of this section on Github.

Displaying the card type icon

This is our last feature. I created the CardIcon component for this, and I'll pass it to the input through the endEnhancer prop.


I will create some tests for the critical parts of the form to make sure we’ll know instantly when they are breaking, which are validations, value formattings, and form submission.

I love using react-native-testing-library for my tests. It lets you create tests similar to user behavior.

I’m also using bdd-lazy-var, the tool I learned about in my last job. I still pick it up on my tests as it helps to describe the test variables in a clean and more readable way.

So I’ll set up a form with useForm and pass it through the FormProvider just like using it on an actual screen. I'll then change input values, test validation results, and check the result react-hook-form returns when I submit the button. Here is the base setup I'll use in all of my test cases:

Testing credit card number validation

I have three assertions in this test case:

  1. The validation is not triggered before I type in 16 characters.
  2. An error is displayed when I enter an invalid credit card number.
  3. The error disappears when I enter a valid card number.

Testing expiration date validation

Testing with passed and valid dates, and checking the validation error is displayed/hidden:

Testing the form submission

Entering correct values to each input and clicking on the submit button. I then expect the onSubmit method is called with the correct and formatted data:


This is the end result

Again, you can find the full version on Github. Please feel free to send me a message over Twitter if you have any feedback or questions.

The Startup

Get smarter at building your thing. Join The Startup’s +799K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store