Forms are an essential part of a mobile app — specifically, to handle user interactions that are available behind an authorization.
To ensure a seamless user experience, a form component consists of more than the input fields that allow users to enter their credentials. This can vary from handling form state, input field validation, handling errors, form submission, and so on.
Formik is an open-source React and React Native library that allows us to handle forms by:
- keeping track of a form’s state;
- handling form submission via reusable methods and handlers (such as
handleChange
,handleBlur
, andhandleSubmit
); - handling validation and error messages out of the box.
In this post, let’s take a look at how to integrate it along with Yup in a React Native app to create forms and validate them. We are also going to cover how to change the focus of one input field to another using a device’s keyboard by forwarding the ref
created using a useRef hook.
The source code is available at this GitHub repository.
Prerequisites
To follow this tutorial, please make sure you are familiarized with JavaScript/ES6 and meet the following requirements on your local dev environment:
- Node.js version >= 12.x.x installed
- Have access to one package manager such as npm or yarn or npx
- expo-cli version installed, or use npx
To learn more about how to set up and run the simulator or the emulator on your local development environment, visit React Native’s official documentation here.
Getting Started
Let’s start by creating a simple React Native app with a new screen: Login.js
.
Create a new React Native project using expo-cli
and then install the dependencies required to build this demo app. Open a terminal window and execute the following commands:
npx expo-cli init formik-example
cd formik-example
yarn add formik yup
Create reusable components
Create a new directory called components/
. In this directory, we are going to keep two form components that are reusable for various types of forms such as Login
or SignUp
.
Let’s start by creating a simple form button component which is a touchable element that allows the user to interact with the device’s screen and perform the next action. It is going to accept two props:
label
: the text label on the button component;onPress
that is going to be handled by the parent component.
Create a new file called Button.js
and add the following snippet:
import React from "react";
import { TouchableOpacity, Text } from "react-native";
export default function Button({ label, onPress }) {
return (
<TouchableOpacity
style={{
borderRadius: 8,
height: 50,
width: 245,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#e94832",
}}
activeOpacity={0.7}
onPress={onPress}
>
<Text
style={{ fontSize: 18, color: "white", textTransform: "uppercase" }}
>
{label}
</Text>
</TouchableOpacity>
);
}
Now, let’s create the second reusable component to let users enter their credentials. Create a new file called TextInput.js
. This component is going to be reused for every input field in a form. It is going to have an icon on the left of the input field to indicate the nature of the input field itself. It is also going to have a placeholder text that tells the user what type of form value is expected.
It is going to accept one prop and that is the name of the icon
. Each input field may have a different icon and other props that are generally used with a TextInput
component in a React Native app. You will see what different props are used on a TextInput
in the next section. For now, use a rest parameter syntax to pass down the ...otherProps
.
import React from "react";
import { TextInput as RNTextInput, View, StyleSheet } from "react-native";
import { Entypo as Icon } from "@expo/vector-icons";
export default function TextInput({ icon, ...otherProps }) {
const validationColor = "#223e4b";
return (
<View
style={{
flexDirection: "row",
alignItems: "center",
height: 48,
borderRadius: 8,
borderColor: validationColor,
borderWidth: StyleSheet.hairlineWidth,
padding: 8,
}}
>
<View style={{ padding: 8 }}>
<Icon name={icon} color={validationColor} size={16} />
</View>
<View style={{ flex: 1 }}>
<RNTextInput
underlineColorAndroid="transparent"
placeholderTextColor="rgba(34, 62, 75, 0.7)"
{...otherProps}
/>
</View>
</View>
);
}
Create a login screen
After setting up the reusable components, let’s use them in a login screen. Start by creating a new directory called screens/
and then, inside it, create a new file called Login.js
. This component file is going to consist of all the necessary elements and business logic behind a login form.
The first input field is going to be for an email. It is going to have properties such as:
- the name of the icon as
icon
. autoCapitalize
is going to be unique to this field since we do not want any characters to auto-capitalize by default.autoCompleteType
provides autocomplete hints from the device, so it can provide an autofill for the particular field. It has different types, but the one we are going to use here is foremail
.keyboardType
is set toemail-address
. It too has different types.keyboardAppearance
allows you to set the keyboard color either to the system’s default or light or dark in the backgroundreturnKeyType
andreturnKeyLabel
determines how the return key should look like and the label on it. There are different values that you can set for it. Some of them are cross-platform and some are OS-specific. For the email input field, let’s set it to “next” since we want the user to enter their email credential and then move on to the next input field by pressing thenext
button. To programmatically move on to the next input field, we are going to handle that later in a different section.
The second input field is going to be for password
. It is going to use similar properties to the ones we used in the input field for email
but with different values. It has a unique property such as secureTextEntry
set to true
which is often used to enter text values that are sensitive, like a password.
Here is the code snippet for the Login
component after creating these two input fields:
import React from "react";
import { Text, View } from "react-native";
import TextInput from "../components/TextInput";
import Button from "../components/Button";
export default function Login() {
return (
<View
style={{
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
}}
>
<Text style={{ color: "#223e4b", fontSize: 20, marginBottom: 16 }}>
Login
</Text>
<View style={{ paddingHorizontal: 32, marginBottom: 16, width: "100%" }}>
<TextInput
icon="mail"
placeholder="Enter your email"
autoCapitalize="none"
autoCompleteType="email"
keyboardType="email-address"
keyboardAppearance="dark"
returnKeyType="next"
returnKeyLabel="next"
/>
</View>
<View style={{ paddingHorizontal: 32, marginBottom: 16, width: "100%" }}>
<TextInput
icon="key"
placeholder="Enter your password"
secureTextEntry
autoCompleteType="password"
autoCapitalize="none"
keyboardAppearance="dark"
returnKeyType="go"
returnKeyLabel="go"
/>
</View>
<Button label="Login" onPress={() => true} />
</View>
);
}
To see the login form in action, run expo start
or yarn start
.
Add Formik to a login form using the useFormik hook
useFormik
is a custom React hook that returns the Formik state and the handler methods to be used in a form component.
To use it, we have to import it from the formik
library in the Login.js
file.
import { useFormik } from "formik";
You might have noticed by now that we are not using useState
to handle the value of each input field in the Login form. The reason behind that is that formik
comes with a property called initialValues
whose value is the object containing form fields.
In the case of the current form, these values are going to be email
and password
. The onSubmit
method accepts a function that has these values as the first argument to handle the form submission. We are going to use these values to verify if the user credentials provided in the demo app are correct. You can also add other handler methods such as navigating to another screen on successful form submission.
In the Login
component, you can add the following.
const { handleChange, handleSubmit, values } = useFormik({
initialValues: { email: "", password: "" },
onSubmit: values =>
alert(`Email: ${values.email}, Password: ${values.password}`),
});
Now, add onChangeText
on both input fields as well as handleSubmit
as the value of onPress
on the Button
component.
// on email input field
onChangeText={handleChange('email')}
// on password input field
onChangeText={handleChange('password')}
// change the value of onPress prop on <Button />
<Button label='Login' onPress={handleSubmit} />
Fill the input fields and press the login button to see an alert box returning these values.
This means the Login form with a custom component is working and Formik has been integrated successfully.
Add validation schema with Yup
The yup
library is useful for managing complex validations when using Formik in either React or React Native apps. Formik supports both synchronous and asynchronous form validation. It has support for schema-based, form-level validation from Yup.
Start by importing it.
import * as Yup from "yup";
Since initialValues
is an object, you have to specify yup.object()
and define the shape of the object. Make sure that, when you’re defining input fields inside the shape, their names correspond to those described in initialValues
.
Each field in this object is supported by a chain of validation methods provided by the Yup API. The type of both email
and password
is going to be “string” since the onChangeText
method returns the values as strings.
Add the following code snippet before the Login
functional component.
const LoginSchema = Yup.object().shape({
email: Yup.string().email("Invalid email").required("Required"),
password: Yup.string()
.min(2, "Too Short!")
.max(10, "Too Long!")
.required("Required"),
});
Using a library like Yup for validation saves a lot of time, especially when you don’t have to define custom validation methods to check for an input field. For example, in the above snippet, using .email()
automatically matches against a regex instead of defining one, to check the validity of an email input field.
To validate input fields based on the schema just defined, let’s add another property to useFormik
called validationSchema
.
const { handleChange, handleSubmit, values } = useFormik({
validationSchema: LoginSchema,
initialValues: { email: "", password: "" },
onSubmit: values =>
alert(`Email: ${values.email}, Password: ${values.password}`),
});
If you press the login button with blank input fields, the app won’t display an error but it won’t submit the form.
Validating input fields
If the user provides wrong credential values (since we are not covering the backend API in this post, it is a good practice to check the validity of credentials on the server-side as well), it’s a good UX practice to indicate the error. In this section, let’s turn the input field border and the left icon color to red if the defined validation schema object doesn’t match.
We will be using errors
, touched
, and handleBlur
to know whether the input field has been touched by the user and, if yes, will pass the prop errors
to the custom TextInput
to display UI changes based on that.
In the Login
component, modify the following:
const { handleChange, handleSubmit, handleBlur, values, errors, touched } =
useFormik({
validationSchema: LoginSchema,
initialValues: { email: "", password: "" },
onSubmit: values =>
alert(`Email: ${values.email}, Password: ${values.password}`),
});
Then, for the email input field, add the following properties:
<TextInput
// ... rest remains same
onBlur={handleBlur("email")}
error={errors.email}
touched={touched.email}
/>
Similarly, modify the password field:
<TextInput
// ... rest remains same
onBlur={handleBlur("password")}
error={errors.password}
touched={touched.password}
/>
Now, go the TextInput
component, and pass new props: error
and touched
.
export default function TextInput({ icon, error, ...otherProps }) {...}
Next, let’s change the value of validationColor
which we have defined in a previous section, based on whether the input field is touched or not and if there is an error or not by using a nested ternary operator.
const validationColor = !touched ? "#223e4b" : error ? "#FF5A5F" : "#223e4b";
Now, go back to the simulator and, without entering the value for any input field, press the Login button. You will find that the border color and the icon color for both input fields turns red.
Try entering new values that satisfy the loginSchema
. Also, see what happens if you touch one input field and move on to the next without entering any value — it will be considered as touched and an error will be shown.
Try to enter a password with more than 10 characters and verify that and error is also shown.
Select the next text input after pressing the “next” button
The last thing we need to do is to add another property on each input field to select the next TextInput
field when pressing the next
button.
Since there are only two input fields, the next
button is shown only in the email input field. This can be done by adding a property onSubmitEditing
on the input field which accepts a callback as value. By creating a new ref
for the password field, we can determine whether the input field in focus at any given time is the password or not. If not, that means it is the email field and we can press the next button to change the focus from the email to the password field.
In the Login.js
file, start by importing the useRef
hook from the React library and, inside the Login component, define the ref
with the initial value of null
.
import React, { useRef } from "react";
//...
export default function Login() {
const password = useRef(null);
// ...
}
Next, add the ref
property to the password input field.
<TextInput
ref={password}
// ...
/>
Then, add onSubmitEditing
to the email input field.
onSubmitEditing={() => password.current?.focus()}
Back to the simulator, you will encounter the following warning.
This can be solved by using a forwardRef
on the custom TextInput
component. Ref forwarding is a technique for automatically passing a ref
through a component to one of its children. In our case, we need to pass the ref
from Login
to the TextInput
component.
Open TextInput.js
and import forwardRef
from the React library.
import React, { forwardRef } from "react";
Then, wrap all contents of the TextInput
functional component with forwardRef
as shown below:
const TextInput = forwardRef(({ icon, error, touched, ...otherProps }, ref) => {
const validationColor = !touched ? "#223e4b" : error ? "#FF5A5F" : "#223e4b";
return (
<View
style={{
flexDirection: "row",
alignItems: "center",
height: 48,
borderRadius: 8,
borderColor: validationColor,
borderWidth: StyleSheet.hairlineWidth,
padding: 8,
}}
>
<View style={{ padding: 8 }}>
<Icon name={icon} color={validationColor} size={16} />
</View>
<View style={{ flex: 1 }}>
<RNTextInput
underlineColorAndroid="transparent"
placeholderTextColor="rgba(34, 62, 75, 0.7)"
ref={ref}
{...otherProps}
/>
</View>
</View>
);
});
export default TextInput;
Now, by pressing the “next” button on the email input field, you can change the focus of the current field to “password”.
The password input field shows a go
button which indicates that, when users have finished entering their credentials, they are ready to submit the form. By adding onSubmitEditing
with the value of handleSubmit
, you submit the form.
Add the following to the password input field in Login.js
:
onSubmitEditing={() => handleSubmit()}
And that’s it! Here is the output you are going to get after completing this step:
Conclusion
Using a form library like Formik gives us many advantages when building and handling forms to provide a pragmatic user experience.
There are many different methods available in the Yup API to add validation schema to a form component. This post covers the basics of Formik and Yup as well as one way of handling errors and touched fields.
The last section where we used ref
to change the focus from one input field to the next using a device’s keyboard is not a must-have but it may be worth considering if you want to provide seamless user experience.