EthicsComputerCourses
Join training +91 90532 10052

Forms & Keyboard Handling

React Native · Lesson 13 of 30 Intermediate 6 min read Updated

By the end of this lesson you will be able to build a complete mobile form: fields with validation, clear error messages, a submit button that waits for valid input, and a layout where the keyboard never hides the field you are typing in.

Forms are harder on phones than on the web. The screen is small, and the on-screen keyboard takes almost half of it. A form that ignores the keyboard is like a shop counter with a pillar in front of it: the customer cannot see what they are signing.

Step 1: one state object for the form

Keep all field values in one object. A single update function changes one field at a time.

React JSX
import { useState } from "react";
import { View, Text, TextInput, Pressable } from "react-native";

export default function DeliveryForm() {
  const [form, setForm] = useState({ name: "", mobile: "", pincode: "" });

  function update(field, value) {
    setForm((f) => ({ ...f, [field]: value }));
  }

  return (
    <View style={{ padding: 24, gap: 12 }}>
      <TextInput placeholder="Full name" value={form.name} onChangeText={(v) => update("name", v)} style={{ borderWidth: 1, padding: 12 }} />
      <TextInput placeholder="Mobile" inputMode="tel" value={form.mobile} onChangeText={(v) => update("mobile", v)} style={{ borderWidth: 1, padding: 12 }} />
      <TextInput placeholder="PIN code" inputMode="numeric" maxLength={6} value={form.pincode} onChangeText={(v) => update("pincode", v)} style={{ borderWidth: 1, padding: 12 }} />
      <Pressable onPress={() => console.log(form)} style={{ backgroundColor: "#087ea4", padding: 14 }}>
        <Text style={{ color: "white", textAlign: "center" }}>Save address</Text>
      </Pressable>
    </View>
  );
}

What you should see: three bordered fields and a blue button. Typing updates the state, and pressing the button logs the whole form object, for example { name: "Kabir", mobile: "9876543210", pincode: "226001" }.

Step 2: validation as a plain function

Validation rules do not need React at all. Write a function that takes the form and returns an object of error messages. An empty object means the form is valid.

Show errors only after the user has left a field (it is touched), or after they press submit. Shouting "Invalid mobile" while someone is still typing the first digit is unfriendly.

The rules and the touched logic are pure React and JavaScript, so try them here. In your app, onBlur and onChangeText on TextInput do the same job as onBlur and onChange below.

React JSXTry it yourself
import { useState } from "react";
import { createRoot } from "react-dom/client";

function validate(form) {
  const errors = {};
  if (form.name.trim().length < 2) errors.name = "Enter your full name";
  if (!/^[6-9]\d{9}$/.test(form.mobile)) errors.mobile = "Enter a 10-digit mobile number";
  if (!/^[1-9]\d{5}$/.test(form.pincode)) errors.pincode = "PIN code has 6 digits and cannot start with 0";
  return errors;
}

console.log(JSON.stringify(validate({ name: "Kabir", mobile: "98765", pincode: "226001" })));

function DeliveryForm() {
  const [form, setForm] = useState({ name: "", mobile: "", pincode: "" });
  const [touched, setTouched] = useState({});
  const [saved, setSaved] = useState(false);
  const errors = validate(form);
  const isValid = Object.keys(errors).length === 0;

  const field = (key, label) => (
    <p>
      <input placeholder={label} value={form[key]}
        onChange={(e) => setForm({ ...form, [key]: e.target.value })}
        onBlur={() => setTouched({ ...touched, [key]: true })} />
      {touched[key] && errors[key] ? <span style={{ color: "crimson" }}> {errors[key]}</span> : null}
    </p>
  );

  function submit() {
    setTouched({ name: true, mobile: true, pincode: true });
    if (isValid) setSaved(true);
  }

  return (
    <div style={{ fontFamily: "sans-serif" }}>
      {field("name", "Full name")}
      {field("mobile", "Mobile")}
      {field("pincode", "PIN code")}
      <button onClick={submit}>Save address</button>
      {saved ? <p>Saved for {form.name}</p> : null}
    </div>
  );
}

createRoot(document.getElementById("root")).render(<DeliveryForm />);
Output
{"mobile":"Enter a 10-digit mobile number"}

Press "Save address" with empty fields: all three errors appear at once. Fill them correctly and press again: "Saved for ..." appears. The errors are derived from the form on every render, so they can never go out of date.

Step 3: keep fields above the keyboard

When the keyboard opens, it can cover the lower fields and the submit button. React Native's KeyboardAvoidingView moves or pads its content to make room.

React JSX
import { KeyboardAvoidingView, ScrollView, Platform, Keyboard, Pressable } from "react-native";

export default function CheckoutScreen() {
  return (
    <KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === "ios" ? "padding" : undefined}>
      <ScrollView contentContainerStyle={{ padding: 24, gap: 12 }} keyboardShouldPersistTaps="handled">
        {/* your TextInputs here */}
        <Pressable onPress={Keyboard.dismiss}>{/* a "Done" button */}</Pressable>
      </ScrollView>
    </KeyboardAvoidingView>
  );
}

What you should see on iPhone: when you tap the last field, the content gets extra bottom padding and you can scroll the field into view above the keyboard.

  • behavior="padding" usually works best on iOS. On Android, test your screen; Expo's guide shows undefined there, and you should try other values if fields are still hidden.
  • keyboardShouldPersistTaps="handled" lets a tap on a button inside the scroll view work while the keyboard is open. Without it, the first tap only closes the keyboard.
  • Keyboard.dismiss() closes the keyboard from code, for example in a "Done" button or before navigating away.

Step 4: a smoother option for real apps

KeyboardAvoidingView is simple but can behave differently on each platform. Expo's documentation recommends react-native-keyboard-controller for forms that must feel polished. It needs a development build (it is not inside Expo Go) and Reanimated, which Expo projects already include.

React JSX
// src/app/_layout.jsx
import { Stack } from "expo-router";
import { KeyboardProvider } from "react-native-keyboard-controller";

export default function RootLayout() {
  return (
    <KeyboardProvider>
      <Stack />
    </KeyboardProvider>
  );
}

// src/app/checkout.jsx
import { KeyboardAwareScrollView, KeyboardToolbar } from "react-native-keyboard-controller";

export default function Checkout() {
  return (
    <>
      <KeyboardAwareScrollView contentContainerStyle={{ padding: 24, gap: 12 }}>
        {/* TextInputs */}
      </KeyboardAwareScrollView>
      <KeyboardToolbar />
    </>
  );
}

What you should see: when you tap any field, the screen scrolls just enough to show it above the keyboard. A toolbar above the keyboard has previous and next arrows and a "Done" button.

Common mistakes

  • Showing errors while the user is still typing the first character. Wait until the field is touched or the form is submitted.
  • Forgetting flex: 1 on KeyboardAvoidingView. It then has no height to adjust.
  • Buttons that need two taps. Add keyboardShouldPersistTaps="handled" to the scroll view.
  • Trusting client validation alone. The server must validate again; the app's checks are for the user's convenience.
  • Storing errors in state. Calculate them from the form values during render instead.

Key points

  • Keep form values in one state object and update one field at a time.
  • Write validation as a plain function that returns an errors object.
  • Show errors after a field is touched or after submit.
  • KeyboardAvoidingView with behavior="padding" on iOS keeps fields visible; test Android separately.
  • keyboardShouldPersistTaps="handled" and Keyboard.dismiss() make keyboards behave.
  • react-native-keyboard-controller gives smoother keyboard handling in development builds.

Exercise

Build a "Sign up for coaching classes" form with name, email and age fields. Rules: name at least 2 characters, email must contain "@" and ".", age must be a whole number from 10 to 60. Show each error under its field after the field loses focus. Disable the submit button (grey) until the form is valid, and wrap the form so the keyboard does not cover the button on iPhone.

Show solution
React JSX
import { useState } from "react";
import { KeyboardAvoidingView, ScrollView, Platform, TextInput, Text, Pressable } from "react-native";

function validate(f) {
  const e = {};
  if (f.name.trim().length < 2) e.name = "Name is too short";
  if (!f.email.includes("@") || !f.email.includes(".")) e.email = "Enter a valid email";
  const age = Number(f.age);
  if (!Number.isInteger(age) || age < 10 || age > 60) e.age = "Age must be 10 to 60";
  return e;
}

export default function SignUp() {
  const [form, setForm] = useState({ name: "", email: "", age: "" });
  const [touched, setTouched] = useState({});
  const errors = validate(form);
  const valid = Object.keys(errors).length === 0;

  const input = (key, props) => (
    <>
      <TextInput value={form[key]} onChangeText={(v) => setForm({ ...form, [key]: v })}
        onBlur={() => setTouched({ ...touched, [key]: true })} style={{ borderWidth: 1, padding: 12 }} {...props} />
      {touched[key] && errors[key] ? <Text style={{ color: "crimson" }}>{errors[key]}</Text> : null}
    </>
  );

  return (
    <KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === "ios" ? "padding" : undefined}>
      <ScrollView contentContainerStyle={{ padding: 24, gap: 8 }} keyboardShouldPersistTaps="handled">
        {input("name", { placeholder: "Name" })}
        {input("email", { placeholder: "Email", inputMode: "email", autoCapitalize: "none" })}
        {input("age", { placeholder: "Age", inputMode: "numeric" })}
        <Pressable disabled={!valid} onPress={() => console.log("Submitted", form)}
          style={{ backgroundColor: valid ? "#087ea4" : "#aaa", padding: 14, marginTop: 8 }}>
          <Text style={{ color: "white", textAlign: "center" }}>Sign up</Text>
        </Pressable>
      </ScrollView>
    </KeyboardAvoidingView>
  );
}

Number("") is 0, which fails the range check, so an empty age is an error. The button is grey and disabled until validate returns an empty object.

Quick Quiz

1. When should a field's error message first appear?

2. Which behavior usually works best for KeyboardAvoidingView on iOS?

3. A button inside a ScrollView needs two taps while the keyboard is open. What fixes it?

4. Why can react-native-keyboard-controller not be used in Expo Go?

Learn React Native with a mentor

Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call