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).
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.
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
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.
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)
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
The post is really informative! It helped a lot, thank you for writing it! One small change I would recommend:
instead of
so that the custom error style can't be overwritten during implementation
Awesome
Thank you for this detailed tutorial! Please send us a PR to include this tutorial under our resource page.
Great post with a good explanation. Cheers!
Use this if you get undefined error object.
Crisp explanation Sankho! Helped me
Thank you
thanks :)