React Native Form Management Tutorial - Building a credit card form
Tags: reactnative, typescript, webdev, react
Published: 2021-01-20 07:52:22

---
title: React Native Form Management Tutorial - Building a credit card form
description: A guide about creating declarative forms in React Native using react-hook-form by building credit card form example.
tags: reactnative,typescript,webdev,react
canonical_url: https://bilir.me/blog/react-native-form-management-tutorial
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/mj0l6bhp85bouxxptfz9.png
published: true
series: Building a React Native Form
---

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](https://react-hook-form.com/) 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](https://github.com/halilb/credit-card-form). I also ported the React Native code into the web thanks to react-native-web. You may play with it on my [blog](https://bilir.me/blog/react-native-form-management-tutorial).

{% youtube 9MbFlq2JW9U %}

## Table of Contents

- [Starting with a simple UI](#starting-with-a-simple-ui)
- [Integrating react-hook-form](#integrating-reacthookform)
  - [Improving reusability](#improving-reusability)
- [Validations](#validations)
  - [Triggering validations](#triggering-validations)
- [Formatting input values](#formatting-input-values)
- [Focusing on the next field](#focusing-on-the-next-field)
- [Displaying the card type icon](#displaying-the-card-type-icon)
- [Testing](#testing)
  - [Testing credit card number validation](#testing-credit-card-number-validation)
  - [Testing expiration date validation](#testing-expiration-date-validation)
  - [Testing the form submission](#testing-the-form-submission)
- [Output](#output)


## Starting with a simple UI

For this tutorial, I used [this clean design](https://dribbble.com/shots/10744785-Daily-UI-001-002-Sign-Up-Credit-Card-Checkout/) I found on Dribbble as the design reference. I've also used the [TextField](https://gist.github.com/halilb/9ac8e43e95ffbda42d52c34d420e78a4#file-textfield-tsx) component I built in my [last post](/halilb/creating-an-animated-textfield-with-react-native-4jh2). Here is the `CreditCardForm` component that generates the UI with simple local state variables:

```tsx
// CreditCardForm.tsx
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import Button from './Button';
import TextField from './TextField';

const CreditCardForm: React.FC = () => {
  const [name, setName] = useState('');
  const [cardNumber, setCardNumber] = useState('');
  const [expiration, setExpiration] = useState('');
  const [cvv, setCvv] = useState('');

  function onSubmit() {
    console.log('form submitted');
  }

  return (
    <View>
      <TextField
        style={styles.textField}
        label="Cardholder Name"
        value={name}
        onChangeText={(text) => setName(text)}
      />
      <TextField
        style={styles.textField}
        label="Card Number"
        value={cardNumber}
        onChangeText={(text) => setCardNumber(text)}
      />
      <View style={styles.row}>
        <TextField
          style={[
            styles.textField,
            {
              marginRight: 24,
            },
          ]}
          label="Expiration Date"
          value={expiration}
          onChangeText={(text) => setExpiration(text)}
        />
        <TextField
          style={styles.textField}
          label="Security Code"
          value={cvv}
          onChangeText={(text) => setCvv(text)}
        />
      </View>
      <Button title="PAY $15.12" onPress={onSubmit} />
    </View>
  );
};

const styles = StyleSheet.create({
  row: {
    flex: 1,
    flexDirection: 'row',
    marginBottom: 36,
  },
  textField: {
    flex: 1,
    marginTop: 24,
  },
});

export default CreditCardForm;
```

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

```tsx
// App.tsx
import React, { useState } from 'react';
import { StyleSheet, Text, ScrollView } from 'react-native';
import CreditCardForm from './components/CreditCardForm';

const App: React.FC = () => {
  return (
    <ScrollView contentContainerStyle={styles.content}>
      <Text style={styles.title}>Payment details</Text>
      <CreditCardForm />
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  content: {
    paddingTop: 96,
    paddingHorizontal: 36,
  },
  title: {
    fontFamily: 'Avenir-Heavy',
    color: 'black',
    fontSize: 32,
    marginBottom: 32,
  },
});

export default App;
```

## 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:

```bash
npm install react-hook-form
// or
yarn 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`:

```jsx
// App.tsx
import React from 'react';
import { View, Text, TextInput } from 'react-native';
import { useForm, Controller } from 'react-hook-form';

export default function App() {
  const { control, handleSubmit, errors } = useForm();
  const onSubmit = (data) => console.log(data);

  return (
    <View>
      <Controller
        control={control}
        render={({ onChange, onBlur, value }) => (
          <TextInput
            style={styles.input}
            onBlur={onBlur}
            onChangeText={(value) => onChange(value)}
            value={value}
          />
        )}
        name="firstName"
        rules={{ required: true }}
        defaultValue=""
      />
      {errors.firstName && <Text>This is required.</Text>}
    </View>
  );
}
```

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:

```tsx
// FormTextField.tsx
import React from 'react';
import { useFormContext, Controller } from 'react-hook-form';
import TextField from './TextField';

type Props = React.ComponentProps<typeof TextField> & {
  name: string;
};

const FormTextField: React.FC<Props> = (props) => {
  const { name, ...restOfProps } = props;
  const { control, errors } = useFormContext();

  return (
    <Controller
      control={control}
      render={({ onChange, onBlur, value }) => (
        <TextField
          // passing everything down to TextField
          // to be able to support all TextInput props
          {...restOfProps}
          errorText={errors[name]?.message}
          onBlur={onBlur}
          onChangeText={(value) => onChange(value)}
          value={value}
        />
      )}
      name={name}
    />
  );
};

export default FormTextField;
```

Now, it's time to migrate our form components to `react-hook-form`. We'll simply replace `TextField`s 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](https://github.com/halilb/credit-card-form/commit/583f960ac2cebee9af9f3e646c9afb0100563c45).

```tsx
// CreditCardForm.tsx
interface FormModel {
  holderName: string;
  cardNumber: string;
  expiration: string;
  cvv: string;
}

const CreditCardForm: React.FC = () => {
  const formMethods = useForm<FormModel>({
    defaultValues: {
      holderName: '',
      cardNumber: '',
      expiration: '',
      cvv: '',
    },
  });

  function onSubmit(model: FormModel) {
    console.log('form submitted', model);
  }

  return (
    <View>
      <FormProvider {...formMethods}>
        <FormTextField
          style={styles.textField}
          name="holderName"
          label="Cardholder Name"
        />
        <FormTextField
          style={styles.textField}
          name="cardNumber"
          label="Card Number"
        />
        <View style={styles.row}>
          <FormTextField
            style={[
              styles.textField,
              {
                marginRight: 24,
              },
            ]}
            name="expiration"
            label="Expiration Date"
          />
          <FormTextField
            style={styles.textField}
            name="cvv"
            label="Security Code"
            keyboardType="number-pad"
          />
        </View>
        <Button
          title="PAY $15.12"
          onPress={formMethods.handleSubmit(onSubmit)}
        />
      </FormProvider>
    </View>
  );
};
```

#### 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:

```diff
// App.tsx
 const App: React.FC = () => {
+  const formMethods = useForm<FormModel>({
+    // to trigger the validation on the blur event
+    mode: 'onBlur',
+    defaultValues: {
+      holderName: 'Halil Bilir',
+      cardNumber: '',
+      expiration: '',
+      cvv: '',
+    },
+  })
+  const cardNumber = formMethods.watch('cardNumber')
+  const cardType = cardValidator.number(cardNumber).card?.niceType
+
+  function onSubmit(model: FormModel) {
+    Alert.alert('Success')
+  }
+
   return (
     <ScrollView contentContainerStyle={styles.content}>
-      <Text style={styles.title}>Payment details</Text>
-      <CreditCardForm />
+      <FormProvider {...formMethods}>
+        <Text style={styles.title}>Payment details</Text>
+        <CreditCardForm />
+        <Button
+          title={cardType ? `PAY $15.12 WITH ${cardType}` : 'PAY $15.12'}
+          onPress={formMethods.handleSubmit(onSubmit)}
+        />
+      </FormProvider>
     </ScrollView>
   )
 }
```

I'll go with the second option.

## Validations

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

```diff
// FormTextField.tsx
-import { useFormContext, Controller } from 'react-hook-form'
+import { useFormContext, Controller, RegisterOptions } from 'react-hook-form'
 import TextField from './TextField'

 type Props = React.ComponentProps<typeof TextField> & {
   name: string
+  rules: RegisterOptions
 }

 const FormTextField: React.FC<Props> = (props) => {
-  const { name, ...restOfProps } = props
+  const { name, rules, ...restOfProps } = props
   const { control, errors } = useFormContext()

   return (
@@ -25,6 +26,7 @@ const FormTextField: React.FC<Props> = (props) => {
         />
       )}
       name={name}
+      rules={rules}
     />
   )
 }
```

For the tutorial, I'll delegate the validations logic to [Braintree's card-validator](https://github.com/braintree/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](https://github.com/halilb/credit-card-form/commit/5ce3f24944788d2043411091619f033e992066fc).

```tsx
// CreditCardForm.tsx
<>
  <FormTextField
    style={styles.textField}
    name="holderName"
    label="Cardholder Name"
    rules={{
      required: 'Cardholder name is required.',
      validate: {
        isValid: (value: string) => {
          return (
            cardValidator.cardholderName(value).isValid ||
            'Cardholder name looks invalid.'
          );
        },
      },
    }}
  />
  <FormTextField
    style={styles.textField}
    name="cardNumber"
    label="Card Number"
    keyboardType="number-pad"
    rules={{
      required: 'Card number is required.',
      validate: {
        isValid: (value: string) => {
          return (
            cardValidator.number(value).isValid ||
            'This card number looks invalid.'
          );
        },
      },
    }}
  />
  <FormTextField
    style={[
      styles.textField,
      {
        marginRight: 24,
      },
    ]}
    name="expiration"
    label="Expiration Date"
    rules={{
      required: 'Expiration date is required.',
      validate: {
        isValid: (value: string) => {
          return (
            cardValidator.expirationDate(value).isValid ||
            'This expiration date looks invalid.'
          );
        },
      },
    }}
  />
  <FormTextField
    style={styles.textField}
    name="cvv"
    label="Security Code"
    keyboardType="number-pad"
    maxLength={4}
    rules={{
      required: 'Security code is required.',
      validate: {
        isValid: (value: string) => {
          const cardNumber = formMethods.getValues('cardNumber');
          const { card } = cardValidator.number(cardNumber);
          const cvvLength = card?.type === 'american-express' ? 4 : 3;

          return (
            cardValidator.cvv(value, cvvLength).isValid ||
            'This security code looks invalid.'
          );
        },
      },
    }}
  />
</>
```

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

![validations](https://dev-to-uploads.s3.amazonaws.com/i/v4601yc21h63b9o8rjbf.png)

#### 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.

```diff
// FormTextField.tsx
type Props = React.ComponentProps<typeof TextField> & {
   name: string
   rules: RegisterOptions
+  validationLength?: number
 }

 const FormTextField: React.FC<Props> = (props) => {
-  const { name, rules, ...restOfProps } = props
-  const { control, errors } = useFormContext()
+  const {
+    name,
+    rules,
+    validationLength = 1,
+    ...restOfProps
+  } = props
+  const { control, errors, trigger, watch } = useFormContext()
+  const value = watch(name)
+
+  useEffect(() => {
+    if (value.length >= validationLength) {
+      trigger(name)
+    }
+  }, [value, name, validationLength, trigger])
```

## 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:

```ts
// utils/formatters.ts
export function cardNumberFormatter(
  oldValue: string,
  newValue: string,
): string {
  // user is deleting so return without formatting
  if (oldValue.length > newValue.length) {
    return newValue;
  }

  return newValue
    .replace(/\W/gi, '')
    .replace(/(.{4})/g, '$1 ')
    .substring(0, 19);
}

export function expirationDateFormatter(
  oldValue: string,
  newValue: string,
): string {
  // user is deleting so return without formatting
  if (oldValue.length > newValue.length) {
    return newValue;
  }

  return newValue
    .replace(/\W/gi, '')
    .replace(/(.{2})/g, '$1/')
    .substring(0, 5);
}
```

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

```diff
// FormTextField.tsx
-  onChangeText={(value) => onChange(value)}
+  onChangeText={(text) => {
+    const newValue = formatter ? formatter(value, text) : text
+    onChange(newValue)
+  }}
   value={value}
  />
)}
```

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:

```ts
// utils/formatters.test.ts
import { cardNumberFormatter, expirationDateFormatter } from './formatters';

describe('cardNumberFormatter', () => {
  test.each([
    {
      // pasting the number
      oldValue: '',
      newValue: '5555555555554444',
      output: '5555 5555 5555 4444',
    },
    {
      // trims extra characters
      oldValue: '',
      newValue: '55555555555544443333',
      output: '5555 5555 5555 4444',
    },
    {
      oldValue: '555',
      newValue: '5555',
      output: '5555 ',
    },
    {
      // deleting a character
      oldValue: '5555 5',
      newValue: '5555 ',
      output: '5555 ',
    },
  ])('%j', ({ oldValue, newValue, output }) => {
    expect(cardNumberFormatter(oldValue, newValue)).toEqual(output);
  });
});

describe('expirationDateFormatter', () => {
  test.each([
    {
      // pasting 1121
      oldValue: '',
      newValue: '1121',
      output: '11/21',
    },
    {
      // pasting 11/21
      oldValue: '',
      newValue: '11/21',
      output: '11/21',
    },
    {
      oldValue: '1',
      newValue: '12',
      output: '12/',
    },
    {
      // deleting a character
      oldValue: '12/2',
      newValue: '12/',
      output: '12/',
    },
  ])('%j', ({ oldValue, newValue, output }) => {
    expect(expirationDateFormatter(oldValue, newValue)).toEqual(output);
  });
});
```

## 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 `ref`s 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](https://reactjs.org/docs/forwarding-refs.html) to make sure `ref` is attached to the native `TextInput`.

Here are the steps I followed to build this:

- Wrapped `FormTextField` and `TextField` with `React.forwardRef`:

```diff
+ import { TextInput } from "react-native"
// components/FormTextField.tsx
-const FormTextField: React.FC<Props> = (props) => {
+const FormTextField = React.forwardRef<TextInput, Props>((props, ref) => {
// components/TextField.tsx
-const TextField: React.FC<Props> = (props) => {
+const TextField = React.forwardRef<TextInput, Props>((props, ref) => {
```

- Created `onValid` prop on `FormTextField` component, and modified the effect that triggers validation:

```diff
// FormTextField.tsx
useEffect(() => {
+    async function validate() {
+      const isValid = await trigger(name)
+      if (isValid) onValid?.()
+    }
+
     if (value.length >= validationLength) {
-      trigger(name)
+      validate()
     }
   }, [value, name, validationLength, trigger])
```

- Created a ref for each component and triggered the next input ref's `onFocus` method:

```diff
// CreditCardForm.tsx
+ const holderNameRef = useRef<TextInput>(null)
+ const cardNumberRef = useRef<TextInput>(null)
+ const expirationRef = useRef<TextInput>(null)
+ const cvvRef = useRef<TextInput>(null)

<>
  <FormTextField
+   ref={holderNameRef}
    name="holderName"
    label="Cardholder Name"
+   onSubmitEditing={() => cardNumberRef.current?.focus()}
  />
  <FormTextField
+   ref={cardNumberRef}
    name="cardNumber"
    label="Card Number"
+   onValid={() => expirationRef.current?.focus()}
  />
  <FormTextField
+   ref={expirationRef}
    name="expiration"
    label="Expiration Date"
+   onValid={() => cvvRef.current?.focus()}
  />
  <FormTextField
+   ref={cvvRef}
    name="cvv"
    label="Security Code"
+   onValid={() => {
+     // form is completed so hide the keyboard
+     Keyboard.dismiss()
+   }}
  />
</>
```

You can check out the full diff of this section on [Github](https://github.com/halilb/credit-card-form/commit/af8a138c1c970efea710f855e000bda25b45dc69).

## 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.

```tsx
// CardIcon.tsx
import React from 'react';
import { Image, StyleSheet } from 'react-native';
import cardValidator from 'card-validator';

const VISA = require('./visa.png');
const MASTERCARD = require('./mastercard.png');
const AMEX = require('./amex.png');
const DISCOVER = require('./discover.png');

type Props = {
  cardNumber: string;
};

const CardIcon: React.FC<Props> = (props) => {
  const { cardNumber } = props;
  const { card } = cardValidator.number(cardNumber);

  let source;
  switch (card?.type) {
    case 'visa':
      source = VISA;
      break;
    case 'mastercard':
      source = MASTERCARD;
      break;
    case 'discover':
      source = DISCOVER;
      break;
    case 'american-express':
      source = AMEX;
      break;
    default:
      break;
  }

  if (!source) return null;

  return <Image style={styles.image} source={source} />;
};

const styles = StyleSheet.create({
  image: {
    width: 48,
    height: 48,
  },
});

export default CardIcon;
```

You can review the full diff for the card icon over [here](https://github.com/halilb/credit-card-form/commit/dce1ddb77dcf518d4dc19f844813aa6f3467e92f).

## Testing

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](https://github.com/callstack/react-native-testing-library) for my tests. It lets you create tests similar to user behavior.

I'm also using [bdd-lazy-var](https://github.com/stalniy/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:

```tsx
// CreditCardForm.test.tsx
import React from 'react';
import { fireEvent, render, waitFor } from '@testing-library/react-native';
import { def, get } from 'bdd-lazy-var/getter';
import { useForm, FormProvider } from 'react-hook-form';
import { Button } from 'react-native';
import CreditCardForm from './CreditCardForm';

const FormWrapper = () => {
  const formMethods = useForm({
    mode: 'onBlur',
    defaultValues: {
      holderName: '',
      cardNumber: '',
      expiration: '',
      cvv: '',
    },
  });
  const { handleSubmit } = formMethods;

  const onSubmit = (model) => {
    get.onSubmit(model);
  };

  return (
    <FormProvider {...formMethods}>
      <CreditCardForm />
      <Button onPress={handleSubmit(onSubmit)} title={'Submit'} />
    </FormProvider>
  );
};

def('render', () => () => render(<FormWrapper />));
def('onSubmit', () => jest.fn());
```

#### 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.

```tsx
// CreditCardForm.test.tsx
it('validates credit card number', async () => {
  const { queryByText, getByTestId } = get.render();

  // does not display validation message until input is filled
  const cardInput = getByTestId('TextField.cardNumber');
  fireEvent.changeText(cardInput, '55555555');
  await waitFor(() => {
    expect(queryByText(/This card number looks invalid./)).toBeNull();
  });

  // invalid card
  fireEvent.changeText(cardInput, '5555555555554440');
  await waitFor(() => {
    expect(queryByText(/This card number looks invalid./)).not.toBeNull();
  });

  // valid card
  fireEvent.changeText(cardInput, '5555 5555 5555 4444');
  await waitFor(() => {
    expect(queryByText(/This card number looks invalid./)).toBeNull();
  });
});
```

#### Testing expiration date validation

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

```tsx
// CreditCardForm.test.tsx
it('validates expiration date', async () => {
  const { queryByText, getByTestId } = get.render();

  const input = getByTestId('TextField.expiration');
  // passed expiration date
  fireEvent.changeText(input, '1018');
  await waitFor(() =>
    expect(queryByText(/This expiration date looks invalid./)).not.toBeNull(),
  );

  // valid date
  fireEvent.changeText(input, '10/23');
  await waitFor(() =>
    expect(queryByText(/This expiration date looks invalid./)).toBeNull(),
  );
});
```

#### 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:

```tsx
// CreditCardForm.test.tsx
it('submits the form', async () => {
  const { getByText, getByTestId } = get.render();

  fireEvent.changeText(getByTestId('TextField.holderName'), 'Halil Bilir');
  fireEvent.changeText(getByTestId('TextField.cardNumber'), '5555555555554444');
  fireEvent.changeText(getByTestId('TextField.expiration'), '0224');
  fireEvent.changeText(getByTestId('TextField.cvv'), '333');

  fireEvent.press(getByText('Submit'));

  await waitFor(() =>
    expect(get.onSubmit).toHaveBeenLastCalledWith({
      holderName: 'Halil Bilir',
      // cardNumber and expiration are now formatted
      cardNumber: '5555 5555 5555 4444',
      expiration: '02/24',
      cvv: '333',
    }),
  );
});
```

## Output

You can find the full version on [Github](https://github.com/halilb/credit-card-form). Please feel free to send me a message over [Twitter](https://twitter.com/hbilirr) if you have any feedback or questions.