Edupala

Comprehensive Full Stack Development Tutorial: Learn Ionic, Angular, React, React Native, and Node.js with JavaScript

Mastering React Native Forms: A Comprehensive Guide

react native custom input component

React Native is a framework that is widely used to develop mobile apps. One of the key features of any mobile app is the ability to collect user input through forms. In this articles we will learn how to implement React native forms.

The native HTML tag are not work in React Native, and is same for <form> also. The React Native does not have a native form element, but you can still create and manage forms using various libraries and techniques.

Here are the main objectives of this article: learn how to implement React Native forms, perform React Native form validation, build custom input components, and explore the best approaches for implementing forms in React Native.

Basic Form Components in React Native

To create a form in React Native, we’ll use a combination of built-in components such as TextInput, Button,and othe component to build custom Input components. The most basic component for collecting user input is the TextInput component and we can used this component to collect text, email and textarea data.

React Native Login Form Example

Let first demonstrate React Native form example on Login Form, and we will also validate the form field both email and password. Here is our firt example screenshot.

React native form validation

When the user submits the form, we’ll typically want to send the data to a server for processing. React Native provides a number of APIs for sending HTTP requests to a server. We can use these APIs to send the data in JSON format to our server. Here in our example, we validate the form email input valide or not and check input password having minimum of 8 character.

import React, { useState } from "react";
import { StyleSheet, Text, TextInput, View, Button } from "react-native";

const LoginForm = () => {
  const [formData, setFormData] = useState({ email: "", password: "" });
  const [validationErrors, setValidationErrors] = useState({
    emailError: null,
    passwordError: null,
  });

  const handleInputChange = (name, value) => {
    setFormData({ ...formData, [name]: value });
  };

  const validateEmail = (email) => {
    const isValid = /^[a-zA-Z0-9.+_-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(
      email
    );
    return isValid;
  };

  const validatePassword = (password) => {
    const isValid = password.length >= 8;
    return isValid;
  };

  const handleFormSubmit = (event) => {
    event.preventDefault(); // Prevent the default form submission

    const { email, password } = formData;
    const isEmailValid = validateEmail(email);
    const isPasswordValid = validatePassword(password);

    if (!isEmailValid) {
      const updatedErrors = {
        emailError: isEmailValid ? null : "Invalid email address",
        passwordError: isPasswordValid
          ? null
          : "Password must be at least 8 characters long",
      };
      setValidationErrors(updatedErrors);
    }

    if (!isPasswordValid) {
      const updatedErrors = {
        emailError: isEmailValid ? null : "Invalid email address",
        passwordError: isPasswordValid
          ? null
          : "Password must be at least 8 characters long",
      };
      setValidationErrors(updatedErrors);
    }

    if (isEmailValid && isPasswordValid) {
      // Submit form data to server or perform other actions
      console.log("Form submitted successfully!");
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.label}>Email:</Text>
      <TextInput
        style={styles.input}
        value={formData.email}
        onChangeText={(text) => handleInputChange("email", text)}
        name="email"
        keyboardType="email-address"
      />
      {validationErrors.emailError && (
        <Text style={styles.errorText}>{validationErrors.emailError}</Text>
      )}

      <Text style={styles.label}>Password:</Text>
      <TextInput
        style={styles.input}
        value={formData.password}
        onChangeText={(text) => handleInputChange("password", text)}
        name="password"
        secureTextEntry={true}
      />
      {validationErrors.passwordError && (
        <Text style={styles.errorText}>{validationErrors.passwordError}</Text>
      )}

      <Button title="Login" onPress={handleFormSubmit} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  label: {
    fontSize: 16,
    fontWeight: "bold",
    marginBottom: 10,
  },
  input: {
    borderWidth: 1,
    borderColor: "#ddd",
    padding: 10,
    marginBottom: 10,
  },
  errorText: {
    color: "red",
    fontSize: 12,
  },
});

export default LoginForm;

React Native Form Validation using Custom Input Component

It is good practice to create a custom input component for all input types in the React native form if possible. In this example, we will create two custom components as follows:

  1. CustomInput: for all inputs, including radio (except checkbox).
  2. CustomCheckbox: for checkbox input.

Here is a screenshot of our React Native custom input components:

react native custom input component
react native custom input component

A custom input component is a reusable component that can be used in multiple places within an application. It should be highly customizable and allow the user to input data in a way that is appropriate for the context.

To create a custom input component in React Native, we need to follow a few simple steps. Lets start create first custom Input component for all input except checkbox. This will give us access to all the functionality of a standard input field, such as the ability to handle user input and display text.

CustomInput: for all inputs, including radio (except checkbox).

We are validating input on the blur event. In our example, for radio inputs, we didn’t use React Native TextInput. Therefore, we first need to check the type of input, whether it’s a radio or another type. For all other types, we use TextInput from React Native, while for radios, we use TouchableOpacity and Text components from React Native.

We can add any customizations we require, such as adding a label or placeholder text, setting the font size, or changing the background color. We can also add any custom input validation logic that is required. Here is the code for our components/CustomInput.jsx

import React, { useState } from "react";
import {
  View,
  StyleSheet,
  TextInput,
  Text,
  TouchableOpacity,
} from "react-native";
import { GlobalStyles } from "../constant/styles";

const CustomInput = ({ onChange, ...props }) => {
  const { type, label, options, value, style, ...restProps } = props;

  const [errorMessage, setErrorMessage] = useState("");

  const onInputHandler = (typeValue) => {
    onChange(typeValue);
  };

  const validateInput = () => {
    setErrorMessage("");

    if (restProps.required && value.trim() === "") {
      setErrorMessage("This field is required");
    } else if (restProps.pattern && !value.match(restProps.pattern)) {
      setErrorMessage(restProps.errorMessage || "Invalid input format");
    }
    console.log("Blur on ", errorMessage);
  };

  const handleBlur = () => {
    validateInput();
  };

  let renderInput;

  if (type === "radio") {
    const [selectedOption, setSelectedOption] = useState(value);

    const handleRadioButtonPress = (optionValue) => {
      setSelectedOption(optionValue);
      onChange(optionValue);
    };

    renderInput = (
      <View>
        {options &&
          options?.map((option) => (
            <TouchableOpacity
              key={option.value}
              onPress={() => handleRadioButtonPress(option.value)}
              style={styles.radioButtonContainer}
            >
              {selectedOption === option.value ? (
                <Text style={styles.radioButtonSelected}>●</Text>
              ) : (
                <Text style={styles.radioButtonUnselected}>○</Text>
              )}
              <Text style={styles.radioButtonLabel}>{option.label}</Text>
            </TouchableOpacity>
          ))}
      </View>
    );
  } else {
    renderInput = (
      <TextInput
        {...restProps}
        keyboardType="default"
        value={value}
        onChangeText={onInputHandler}
        onBlur={handleBlur}
        style={styles.input}
      />
    );
  }

  return (
    <View style={[styles.inputContainer, style]}>
      {label && <Text style={styles.label}>{label}</Text>}
      {renderInput}
      {errorMessage && <Text style={styles.errorMessage}>{errorMessage}</Text>}
    </View>
  );
};

const styles = StyleSheet.create({
  inputContainer: {
    marginHorizontal: 4,
    marginVertical: 6,
  },
  input: {
    backgroundColor: GlobalStyles.colors.secondary,
    color: "black",
    padding: 6,
    borderRadius: 6,
    fontSize: 18,
  },
  label: {
    fontSize: 16,
    marginBottom: 5,
  },
  radioButtonContainer: {
    flexDirection: "row",
    alignItems: "center",
    marginHorizontal: 0,
  },
  radioButtonSelected: {
    fontSize: 40,
    marginRight: 10,
  },
  radioButtonUnselected: {
    fontSize: 40,
    marginRight: 10,
    color: "red",
  },
  radioButtonLabel: {
    fontSize: 16,
  },

  errorMessage: {
    color: "red",
    fontSize: 12,
    fontStyle: "italic",
    marginTop: 5,
  },
});

export default CustomInput;

CustomCheckbox: for checkbox input

I tried to add a checkbox inside our previous custom input component, but most of the logic is different from the other inputs. Let’s create a separate component for the checkbox, and here is the code for our custom checkbox inside components/CustomCheckbox.jsx

import { TouchableOpacity, View, Text, StyleSheet } from "react-native";
import { Ionicons } from "@expo/vector-icons";

const CustomCheckbox = ({ label, isChecked, onChange }) => {
  return (
    <TouchableOpacity onPress={onChange} style={styles.checkboxContainer}>
      <View style={[styles.checkbox, isChecked && styles.checked]} />
      <Ionicons
        style={styles.checkbox}
        name="checkbox"
        size={24}
        color={isChecked ? "black" : "#e9e9e9"}
      />
      <Text style={styles.label}>{label}</Text>
    </TouchableOpacity>
  );
};

export default CustomCheckbox;
const styles = StyleSheet.create({
  checkboxContainer: {
    flexDirection: "row",
    alignItems: "center",
    marginTop: 8,
    marginLeft: 8,
  },
  checkbox: {
    marginRight: 8,
  },
  label: {
    fontSize: 14,
  },
});

This CustomCheckbox component is our second custom input reusable component a for rendering a checkbox. It takes three props:

  • label: The text label for the checkbox.
  • isChecked: A boolean indicating whether the checkbox is checked.
  • onChange: A callback function to handle the checkbox state change.

In our React native form component, where it is used, we will include this custom checkbox component for each checkbox option. In our example form, we call this custom component four times to facilitate the selection of framework options, as shown in the figure.

The component uses TouchableOpacity to create a touchable area for the checkbox. It displays the label, and if the checkbox is checked, it shows a checkmark symbol (“✔”). The styling is done using the StyleSheet from React Native.

This component is utilized in the our cusomt Forms component to allow users to select frameworks they are familiar with. The onCheckboxChange function is responsible for updating the state of the selected frameworks when a checkbox is clicked.

Consuming our custom inputs

Once we have our custom input, we can reused this custom component at multiple location in our application. Let used it our our custom form component called Forms inside the components folder.

import { useState } from "react";
import { Button, StyleSheet, ScrollView, Text, View } from "react-native";
import CustomInput from "./CustomInput";
import { GlobalStyles } from "../constant/styles";
import CustomCheckbox from "./CustomCheckbox";

const Forms = () => {
  const [fname, setFname] = useState("");
  const [lname, setLname] = useState("");
  const [email, setEmail] = useState("");
  const [gender, setGender] = useState("male");
  const [address, setAddress] = useState("");
  const [frameworks, setFramworks] = useState({
    Reactjs: false,
    Angular: false,
    Laraval: false,
    Vue: false,
  });

  const inputs = [
    {
      name: "fname",
      value: fname,
      label: "First Name",
      type: "text",
      placeholder: "First Name",
      pattern: "^[A-Za-z0-9]{4,20}$",
      required: true,
      errorMessage:
        "First should be atlest 4 to 16 characters, mustn't have special character",
    },
    {
      name: "lname",
      value: lname,
      label: "Last Name",
      type: "text",
      placeholder: "Last Name",
    },
    {
      id: "1",
      name: "email",
      value: email,
      label: "Email",
      type: "email",
      placeholder: "Email",
      pattern:
        /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$/,

      required: true,
      errorMessage: "Please enter a valid email address!",
    },
    {
      id: "2",
      name: "gender",
      label: "Select your gender",
      type: "radio",
      value: "male",
      options: [
        { value: "male", label: "Male" },
        { value: "female", label: "Female" },
      ],
    },
    {
      id: "3",
      name: "address",
      value: address,
      label: "Address",
      type: "textarea",
      placeholder: "Address",
      multiline: true,
      numberOfLines: 3,
      required: true,
      errorMessage: "Please enter address ",
    },
  ];

  const inputHandler = (name, value) => {
    if (name === "fname") setFname(value);
    else if (name === "lname") setLname(value);
    else if (name === "email") setEmail(value);
    else if (name === "gender") setGender(value);
    else if (name === "address") setAddress(value);
  };

  const onSubmit = (event) => {
    event.preventDefault();

    // Submit the form data to a server or perform local operation
    console.log("Form submitted:", { fname, lname, email, gender, address });
    // Reset form data and errors
    setFname("");
    setLname("");
    setEmail("");
    setGender("male");
    setAddress("");
  };

  const onCheckboxChange = (checkboxKey) => {
    setFramworks((prevCheckboxes) => ({
      ...prevCheckboxes,
      [checkboxKey]: !prevCheckboxes[checkboxKey],
    }));
    console.log("Checkbox ", frameworks)
  };

  return (
    <ScrollView style={styles.scrollView}>
      <View style={styles.formContainer}>
        <View>
          <Text style={styles.mainLabel}>User Form</Text>
          <View style={styles.inputsRow}>
            <CustomInput
              {...inputs[0]}
              onChange={(text) => inputHandler("fname", text)}
              style={styles.inputColumn}
            />
            <CustomInput
              {...inputs[1]}
              onChange={(text) => inputHandler("lname", text)}
              style={styles.inputColumn}
            />
          </View>
        </View>

        {inputs?.slice(2).map((input) => (
          <CustomInput
            key={input.id}
            {...input}
            onChange={(text) => inputHandler(input.name, text)}
          />
        ))}
        <View style={styles.checkboxContainer}>
          <Text style={styles.checkboxMainLabel}>Select the framworks you knows</Text>
          {Object.keys(frameworks).map((framework) => (
            <CustomCheckbox
              key={framework}
              label={framework}
              isChecked={frameworks[framework]}
              onChange={() => onCheckboxChange(framework)}
            />
          ))}
        </View>

        <Button title="Submit" style={styles.button} onPress={onSubmit} />
      </View>
    </ScrollView>
  );
};

export default Forms;

const styles = StyleSheet.create({
  scrollView: {
    flex: 1,
    backgroundColor: GlobalStyles.colors.primary,
  },
  formContainer: {
    marginTop: 20,
    marginHorizontal: 6,
    padding: 20,
  },
  mainLabel: {
    fontSize: 18,
    fontWeight: "bold",
  },
  inputsRow: {
    flexDirection: "row",
    justifyContent: "space-between",
  },
  inputColumn: {
    flex: 1,
  },
  button: {
    minWidth: 120,
    marginHorizontal: 8,
  },

  checkboxContainer: {
    marginBottom: 8
  },
  errorMessage: {
    color: "red",
    fontSize: 12,
    fontStyle: "italic",
    marginTop: 5,
  },
  checkboxMainLabel: {
    fontSize: 16
  }
});
 

React Native UseForm library and react native form validation

Form validating user input is a critical aspect of form development. React Native offers various validation techniques, including regular expressions and third-party libraries like Formik, useForm and Yup. These libraries simplify form validation, making it easier to define validation rules, display error messages, and handle form submission.

In our example lets use useForm, it is one of popular and most used libary for managing, validating for form. The UseForm: A lightweight React Hook library for efficient and flexible form state management in React applications.”

Managing form data efficiently is essential for a responsive and dynamic user experience. React Native utilizes state management to achieve this. By utilizing React’s state and setState method, developers can capture user input, validate it, and update the form state accordingly. This enables real-time feedback to users and ensures data integrity.

React native form validation using useForm

here is step for validating react native form. First, make sure you have the library installed:

npm install react-hook-form

We need to import and initialize the useForm.

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

...
const { control, handleSubmit, formState: { errors } } = useForm();

Here is our completed code for our react native useForm.

import React from "react";
import { StyleSheet, Text, View, TextInput, Button } from "react-native";
import { useForm, Controller } from "react-hook-form";

const UseForm = () => {
  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm({
    mode: "onBlur",
    defaultValues: {
      name: "",
      email: "",
      age: "20",
      password: "",
    },
  });

  const onSubmit = (data) => {
    console.log("Form Data: ", data);

    if (errors.name) {
      console.log("Please enter a valid name");
    } else if (errors.email) {
      console.log("Please enter a valid email address");
    } else if (errors.age) {
      console.log("Please enter a valid age");
    } else if (errors.password) {
      console.log("Please enter a strong password");
    } else {
      console.log("Form submitted successfully!");
    }
  };
  return (
    <View style={styles.container}>
      <View style={styles.inputContainer}>
        <Text style={styles.label}>Name</Text>
        <Controller
          control={control}
          rules={{
            required: "Name field is required",
            minLength: {
              value: 5,
              message: "Please enter more than 4 characters",
            },
          }}
          render={({ field: { onChange, onBlur, value } }) => (
            <TextInput
              style={styles.input}
              placeholder="Name"
              onBlur={onBlur}
              onChangeText={onChange}
              value={value}
            />
          )}
          name="name"
        />
        {errors.name && (
          <Text style={styles.errorMsg}>{errors.name.message}</Text>
        )}
      </View>

      <View style={styles.inputContainer}>
        <Text style={styles.label}>Email</Text>
        <Controller
          control={control}
          rules={{
            required: "Email field is required",
            pattern: {
              value: /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
              message: "Please enter a valid email address",
            },
          }}
          render={({ field: { onChange, onBlur, value } }) => (
            <TextInput
              style={styles.input}
              placeholder="Enter email"
              onBlur={onBlur}
              onChangeText={onChange}
              value={value}
            />
          )}
          name="email"
        />
        {errors.email && (
          <Text style={styles.errorMsg}>{errors.email.message}</Text>
        )}
      </View>

      <View style={styles.inputContainer}>
        <Text style={styles.label}>Age</Text>
        <Controller
          control={control}
          rules={{
            required: true,
            pattern: {
              value: /^[0-9]*$/,
              message: "Please enter a valid number",
            },
          }}
          render={({ field: { onChange, onBlur, value } }) => (
            <TextInput
              style={styles.input}
              onBlur={onBlur}
              onChangeText={onChange}
              value={value}
              keyboardType="numeric"
            />
          )}
          name="age"
        />
        {errors.age && (
          <Text style={styles.errorMsg}>{errors.age.message}</Text>
        )}
      </View>

      <View style={styles.inputContainer}>
        <Text style={styles.label}>Password</Text>
        <Controller
          control={control}
          rules={{
            required: "Password field is required",
            pattern: {
              value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,20}$/,
              message:
                "Password should be 8-20 characters with at least 1 uppercase, 1 lowercase, and 1 number",
            },
          }}
          render={({ field: { onChange, onBlur, value } }) => (
            <TextInput
              style={styles.input}
              onBlur={onBlur}
              onChangeText={onChange}
              value={value}
            />
          )}
          name="password"
        />
        {errors.password && (
          <Text style={styles.errorMsg}>{errors.password.message}</Text>
        )}
      </View>
      <Button
        title="Submit"
        onPress={handleSubmit(onSubmit)}
        style={styles.submitButton}
        accessibilityLabel="Submit Button"
      />
    </View>
  );
};

export default UseForm;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: 10,
    paddingHorizontal: 20,
    justifyContent: "center",
  },
  inputContainer: {
    marginBottom: 20,
  },
  label: {
    marginBottom: 5,
    fontSize: 16,
    fontWeight: "bold",
  },
  input: {
    borderWidth: 1,
    borderColor: "#ccc",
    borderRadius: 5,
    padding: 10,
    fontSize: 16,
  },
  errorMsg: {
    color: "red",
    fontSize: 12,
    marginTop: 5,
  },
  submitButton: {
    marginTop: 20,
    padding: 10,
    backgroundColor: "blue",
    borderRadius: 5,
  },
});

Here is i came to note that add true on required attribute on Controller will not generate error message when we remove all the input from the TextInput component. So it is better to put text message instead of boolean true.

Note: Here is what I have noted: adding true to the required attribute on the Controller will not generate an error message when we remove all the input from the TextInput component. So it is better to put a text message instead of a boolean true.

Conclusion
We can easily create forms in the React Native by combination of built-in and custom components. If you are using UI libraries like react native paper or element, then it is best to use input components provided by the UI libraries. By following best practices for form design and using the appropriate components, we can create forms that are both easy to use and effective at collecting user input.

Related Articles

  1. React Native Drawers: Best Practices and Examples
  2. https://edupala.com/using-react-native-tabs-to-create-user-friendly-app/

For instance, TextInput allows users to enter text, while Picker offers a dropdown menu for selecting options.

Mastering React Native Forms: A Comprehensive Guide

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top