Lesson 11 of 26
TextInput and Forms
TextInput's controlled pattern, keyboard-related props, and multi-field form state -- then the plain TypeScript validation logic that behaves the same on any platform.
A form is where a React Native app first has to take real, messy input from
a person instead of just displaying data. TextInput is the component that
does it, and — like most of this framework — it looks close enough to a web
<input> to be dangerous if you assume it's identical.
The controlled pattern
TextInput is controlled the same way a web input can be: a value prop
tells it what to display, and a callback tells you when the user changed it.
The difference is in the callback's shape.
import { useState } from "react";
import { TextInput } from "react-native";
function EmailField() {
const [email, setEmail] = useState("");
return (
<TextInput
value={email}
onChangeText={setEmail}
placeholder="you@example.com"
/>
);
}Notice it's onChangeText, not onChange. On the web, <input onChange={e => setEmail(e.target.value)}> hands you a synthetic event, and you dig the
string out of e.target.value yourself. React Native skips that: onChangeText
calls your handler with the new string directly, which is why setEmail
can be passed straight in as the handler above with no wrapper function.
There is an onChange on TextInput too, but it carries a native event
object (mainly useful for things like tracking selection) — for "what did
the user type," onChangeText is what you want almost every time.
Keyboard-related props
A handful of props steer the on-screen keyboard and how it treats what's typed, and getting them right is most of what separates a form that feels native from one that feels like it was built for a mouse:
<TextInput
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TextInput
value={password}
onChangeText={setPassword}
secureTextEntry
autoCapitalize="none"
/>keyboardTypeswaps the on-screen keyboard layout —"email-address"surfaces@prominently,"numeric"or"phone-pad"show a number pad instead of the full alphabet, and so on.secureTextEntrymasks each typed character, the standard behavior for a password field.autoCapitalizecontrols whether RN auto-capitalizes as the user types. Its default is meant for writing prose, which is exactly wrong for an email address or username —"none"turns it off.
None of these change what value ends up in state. They only change how the
keyboard behaves while the user is producing that value, which is worth
keeping straight: a bug in one of these props makes typing feel wrong,
while a bug in your onChangeText wiring makes the data wrong.
Multi-field form state
One useState per field works fine for a couple of fields:
function SignupForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
return (
<>
<TextInput value={email} onChangeText={setEmail} keyboardType="email-address" autoCapitalize="none" />
<TextInput value={password} onChangeText={setPassword} secureTextEntry autoCapitalize="none" />
</>
);
}As the field count grows, a single object is usually cleaner than a pile of
parallel useState calls, updating one key at a time:
function SignupForm() {
const [values, setValues] = useState({ email: "", password: "" });
const setField = (key: keyof typeof values) => (text: string) =>
setValues((prev) => ({ ...prev, [key]: text }));
return (
<>
<TextInput value={values.email} onChangeText={setField("email")} keyboardType="email-address" autoCapitalize="none" />
<TextInput value={values.password} onChangeText={setField("password")} secureTextEntry autoCapitalize="none" />
</>
);
}Either shape works. What matters is that by the time you're ready to
validate the form, you have a plain object of strings — { email, password } — with no remaining trace of which component or platform produced them.
Validation is just TypeScript
This is the useful realization: once you have that plain object, checking it for problems has nothing to do with React Native at all. It's the same kind of function you'd write to validate a web form, a CLI's input, or a test fixture.
function validateSignupForm(values: {
email: string;
password: string;
}): Record<string, string> {
const errors: Record<string, string> = {};
if (!values.email.includes("@")) {
errors.email = "Enter a valid email address.";
}
if (values.password.length < 8) {
errors.password = "Password must be at least 8 characters.";
}
return errors;
}It takes strings in, returns a map of field-name to error message out (an
empty object means "no errors"). Nothing about the check for @ or the
length check cares whether values.email was typed into a TextInput on a
phone or an <input> in a browser — it's the same string either way by the
time it reaches this function.
The validation logic is plain TypeScript and behaves identically whether the
values came from a React Native TextInput, a web <input>, or a hardcoded
object in a test file. This is worth internalizing as a habit: keep the part
that's genuinely platform-specific (rendering the fields) as thin as
possible, and push the part that isn't (deciding whether the values are
valid) into ordinary functions you could unit test without a device or
simulator running at all.
What to remember
TextInputis controlled viavalueandonChangeText— the callback receives the string directly, unlike the web's event-basedonChange.keyboardType,secureTextEntry, andautoCapitalizeshape how the keyboard behaves, not what ends up in your state.- For more than a couple of fields, a single values object usually beats one
useStateper field. - Validation logic that only deals with plain values (strings, numbers) is ordinary TypeScript — it doesn't belong to React Native and doesn't need a TextInput to be tested.
- Keep the platform-specific part (rendering fields) thin, and push decisions (is this valid?) into plain functions.
Check yourself
4 questions · pass 3/4 to unlock FlatList and Virtualization
1.How does TextInput's controlled pattern differ from the web's <input> element?
2.Which prop hides typed characters, as you'd want for a password field?
3.Why write signup form validation as a plain function that only takes { email, password } values, rather than something wired directly into a specific TextInput?
4.What does autoCapitalize="none" do?
4 left to answer