DEV Community

Cover image for Using React Hook Form with react-native - Part I (set up & validation)
Sankhadeep Roy
Sankhadeep Roy

Posted on • Updated on

Using React Hook Form with react-native - Part I (set up & validation)

Forms in react have always been a sore point. I personally have tried a lot of solutions (redux-form, lifting state up etc), but never really enjoyed working with them. Thankfully things are a lot better now with Formik and React Hook Form.

There are quite a few examples/tutorials of React Hook Form (to be called as RHF) with react for web, so in this post, we'll learn how to set up and use RHF with react-native forms.

Let us start by creating a react-native app and installing the dependencies (I'll be using Expo, feel free to use react-native init).

expo init form-example
cd form-example && yarn add react-hook-form react-native-tailwindcss

We'll now build a basic form with two inputs, name and email. Let's create the two components that we will use in this example. In the project root, create a folder called components. Create 2 files called Button.js and Input.js.

Button.js
// Button.js

import React from 'react';
import { TouchableOpacity, Text } from 'react-native';
import { t } from 'react-native-tailwindcss';

export default function Button({ label, ...props }) {
  return (
    <TouchableOpacity activeOpacity={0.8} {...props} style={styles.button}>
      <Text style={styles.buttonLabel}>{label}</Text>
    </TouchableOpacity>
  );
}

const styles = {
  button: [t.selfStretch, t.bgGreen600, t.itemsCenter, t.pY3, t.rounded],
  buttonLabel: [t.textWhite, t.textLg]
};

Input.js
// Input.js

import React from 'react';
import { View, Text, TextInput } from 'react-native';
import { t } from 'react-native-tailwindcss';

export default function Input(props) {
  return (
    <View style={styles.wrapper}>
      <TextInput
        style={[styles.input, props.error && t.borderRed500, props.style]}
        {...props}
      />
      {props.errorText && (
        <Text style={styles.errorText}>{props.errorText}</Text>
      )}
    </View>
  );
}

const styles = {
  wrapper: [t.selfStretch, t.mB5],
  input: [
    t.h11,
    t.border,
    t.selfStretch,
    t.p2,
    t.borderGray500,
    t.rounded,
    t.textBase,
    t.textGray700
  ],
  errorText: [t.mT1, t.textRed500]
};

Let us now replace the contents of the App.js file with the following

//  App.js

import React, { useState } from 'react';
import { StyleSheet, Switch, Text, View } from 'react-native';
import { t, color } from 'react-native-tailwindcss';

import Input from './components/Input';
import Button from './components/Button';

export default function App() {
  const [isBillingDifferent, setIsBillingDifferent] = useState(false);

  const toggleBilling = () => {
    setIsBillingDifferent((prev) => !prev);
  };

  return (
    <View style={styles.container}>
      <Input placeholder="Name" />
      <Input placeholder="Email" />
      <View style={styles.switch}>
        <Text style={styles.switchText}>Billing different</Text>
        <Switch
          trackColor={{ false: color.gray200, true: color.green600 }}
          thumbColor={color.gray100}
          ios_backgroundColor={color.gray800}
          onValueChange={toggleBilling}
          value={isBillingDifferent}
        />
      </View>
      {isBillingDifferent && (
        <>
          <Input placeholder="Billing name" />
          <Input placeholder="Billing email" />
        </>
      )}
      <Button label="Submit" />
    </View>
  );
}

const styles = {
  container: [t.flex1, t.justifyCenter, t.itemsCenter, t.p6, t.bgGray200],
  switch: [t.mB4, t.selfStart, t.flexRow, t.itemsCenter],
  switchText: [t.textBase, t.mR3, t.textGray800]
};

Now when we run our app, we should see something like this, note that we have a switch which toggles between showing 2 extra fields (we'll use them in Part II of this article).

Simulator Screen Shot - iPhone 11 - 2020-09-16 at 20.55.39

So we have got our basic UI setup done, let us now add RHF to our app. Add the following line below your last import

import { useForm, Controller } from 'react-hook-form';

We now use the useForm hook (inside our component) to get the handleSubmit and control values.

// export default function App() {
const { handleSubmit, control } = useForm();

Using RHF with react-native is a bit different than react for web. With react, we can register an input through its ref (or inputRef in case of some component libraries).
However, in the case of react-native, we need to use the Controller component and the render our Input inside a renderProp. We also need to give it a name and pass it a control prop. Let's change our code accordingly and see how it looks like

<Controller
    name="name"
    control={control}
    render={({ onChange, value }) => (
        <Input
          onChangeText={(text) => onChange(text)}
          value={value}
          placeholder="Name"
        />
    )}
  />

We do the same for our Email field and replace by the name and placeholder props accordingly.

At this point when we run our app, we'll probably get a warning prompting us to add a defaultValue for our fields. Let us add the defaultValues for the fields

//<Controller
    defaultValue=""
//  name="name"

//<Controller
    defaultValue=""
//  name="email"

So, now that we have wired up our form with RHF, let's log these values on press of the Submit button. To do so, we need to wire up handleSubmit (from the useForm hook) to the onPress of our button. Inside handleSubmit we pass our onSubmit function.
In the onSubmit function, we will log the values entered.

<Button onPress={handleSubmit(onSubmit)} label="Submit" />

// onSubmit method
const onSubmit = (data) => {
  console.log(data, 'data');
};

Now when we enter some values and press the button, we should see something like this in our logs.

Screen Shot 2020-09-19 at 1.15.58 PM

So far so good! Let's add some validation to our fields and notify the user when the fields are not filled.
First, we need to add rules our field controllers and then we will use the errors object from the useForm hook to check for any errors in our form.

// export default function App() {
const { handleSubmit, control, errors } = useForm();

// name controller
// control={control}
rules={{
    required: { value: true, message: 'Name is required' }
  }}

// email controller
// control={control}
rules={{
    required: { value: true, message: 'Email is required' }
  }}

Note that we can also use rules={{required: true}} and set the error message separately. Let us now add the error and errorText props to our Input component.

// name input
<Input
    error={errors.name}
    errorText={errors?.name?.message}
 // onChangeText={(text) => onChange(text)}


// email input
<Input
    error={errors.email}
    errorText={errors?.email?.message}
 // onChangeText={(text) => onChange(text)}

Well done! If we now press the submit button without filling the fields, we should see something like this

Simulator Screen Shot - iPhone 11 - 2020-09-24 at 20.51.59

One last thing! Let us also add a check that only allows valid email ids to be submitted. So we add another rule to our email field called pattern.
The name itself is pretty self explanatory, so we'll need an email regex to validate our input with. (I totally didn't copy the regex from here!)

// After the last import statement
const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

// email controller
// required: { value: true, message: 'Email is required' },
   pattern: {
     value: EMAIL_REGEX,
     message: 'Not a valid email'
   }

Great! Now we have successfully added email validation to our form.

Simulator Screen Shot - iPhone 11 - 2020-09-24 at 21.11.31

In the next part, we will learn how to populate our input fields with data from backend API and edit it. We'll also take a look at how to do conditional fields (fields based on user input).

Thanks for reading and do give it a ❤️ if you found it useful!
Happy coding!

Top comments (8)

Collapse
 
dmfdiogo profile image
Diogo Magrini Furmann

for those using the latest version of react hook form an error will popup regarding onChange function..
in the controller render function, you're looking for

render={({ field: { onChange, value } }) => (
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tahaattari profile image
Taha • Edited

The post is really informative! It helped a lot, thank you for writing it! One small change I would recommend:

      <TextInput
        {...props}
        style={[styles.input, props.error && t.borderRed500, props.style]}
      />
Enter fullscreen mode Exit fullscreen mode

instead of

      <TextInput
        style={[styles.input, props.error && t.borderRed500, props.style]}
        {...props}
      />
Enter fullscreen mode Exit fullscreen mode

so that the custom error style can't be overwritten during implementation

Collapse
 
runosaduwa profile image
Runo-saduwa

Awesome

Collapse
 
bluebill1049 profile image
Bill • Edited

Thank you for this detailed tutorial! Please send us a PR to include this tutorial under our resource page.

Collapse
 
sameeramadushan profile image
Sameera Madushan

Great post with a good explanation. Cheers!

Collapse
 
aruna_c73a2ef82adf970a profile image
Aruna BP

Use this if you get undefined error object.

const { handleSubmit, control, formState: { errors } } = useForm();
Enter fullscreen mode Exit fullscreen mode
Collapse
 
supriya_kalghatgi profile image
Supriya Kalghatgi

Crisp explanation Sankho! Helped me
Thank you

Collapse
 
lindagattino profile image
Linda

thanks :)