Forms & Keyboard Handling
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.
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.
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 />);{"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.
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 showsundefinedthere, 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.
// 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: 1onKeyboardAvoidingView. 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.
KeyboardAvoidingViewwithbehavior="padding"on iOS keeps fields visible; test Android separately.keyboardShouldPersistTaps="handled"andKeyboard.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
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?
Waiting until a field is touched avoids scolding users while they are still typing.
2. Which behavior usually works best for KeyboardAvoidingView on iOS?
Expo's guide notes that "padding" usually works best on iOS.
3. A button inside a ScrollView needs two taps while the keyboard is open. What fixes it?
This prop lets taps reach buttons while the keyboard stays open.
4. Why can react-native-keyboard-controller not be used in Expo Go?
Libraries with native code outside Expo Go's set need a development build.
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
