Forms are where a mobile app is won or lost. A form that shows the wrong keyboard, capitalises an email address or hides the submit button behind the keyboard is one users abandon — and every one of those is a prop you forgot.
TextInput is controlled, and that is all
<TextInput
value={value}
onChangeText={onChangeText}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholderTextColor={theme.colors.textSubtle}
accessibilityLabel={label}
style={[styles.input, focused && styles.inputFocused, Boolean(error) && styles.inputError]}
{...inputProps}
/>Note onChangeText, which hands you the string directly — there is no
event.target.value. There is an onChange too and you almost never want it.
Note also placeholderTextColor. There is no ::placeholder
pseudo-element, so it is a prop. Same story for the focus ring: no :focus selector, so
the component keeps a focused boolean in state and swaps a border colour. That is
what the local useState is for.
The props that actually matter
These are the difference between a pleasant form and an infuriating one, and none of them has a web equivalent you can rely on.
keyboardType changes the keyboard the OS shows.
'email-address' puts "@" on the main layout. 'number-pad' gives digits only
— use it for a ZIP code, where 'numeric' would offer a decimal point that can never be
valid. 'phone-pad' for phone numbers.
autoCapitalize defaults to 'sentences'. On an email
field that capitalises the first letter and the server then rejects the address. Set
'none' on emails, usernames and passwords; 'words' on names;
'characters' on a two-letter state code. ⚠️ This is the single most common
React Native form bug and it is invisible on a simulator with a hardware keyboard.
textContentType (and autoComplete on Android) is what
unlocks autofill. 'emailAddress', 'password', 'postalCode'.
Get it right and iOS offers the saved credential above the keyboard; omit it and the user retypes
a password they have stored. Use 'newPassword' on a registration field — that is what
makes iOS offer to generate a strong one.
returnKeyType and onSubmitEditing. There is no Enter
key and no <form> element, so submitting from the keyboard is something you wire.
returnKeyType="go" labels the return key, onSubmitEditing fires when it is
tapped.
secureTextEntry masks the input. There is no
type="password".
One platform quirk worth knowing
input: {
borderWidth: 1.5,
borderColor: theme.colors.border,
borderRadius: theme.radius.sm,
paddingHorizontal: theme.spacing.md,
height: 46,
fontSize: theme.fontSize.base,
color: theme.colors.text,
backgroundColor: theme.colors.surface,
},A fixed height with horizontal padding, rather than padding on all four sides.
Android centres text vertically inside a TextInput and iOS does not, so a single
padding value produces two visibly different fields. Fixing the height is the reliable
way to make them match.
Validation without a library
There is no <form>, no required attribute and no constraint
validation API. Whatever you want, you write — which for a handful of fields is less than pulling in
a form library:
export function validateCheckout(
values: CheckoutFormValues,
options: { orderType: OrderType; needsTypedAddress: boolean },
): CheckoutFieldErrors {
const errors: CheckoutFieldErrors = {};
if (!values.customerName.trim()) {
errors.customerName = 'Please tell us who the order is for.';
}
if (!EMAIL_PATTERN.test(values.email.trim())) {
errors.email = 'We need a valid email to send the receipt.';
}A plain function taking values and returning errors. It imports nothing, touches no hook and is testable in milliseconds — which is why the whole rule set lives outside the component.
The options argument is doing real work: the address fields are only required for
delivery, and only when the customer has not picked a saved address. Conditional requirements are
the usual reason hand-written validation beats a schema.
When to show an error
Two rules make a form feel considerate rather than hostile.
Nothing before the first submit. A form that turns red as you tab into it is
shouting at you for not having typed yet. The demo app keeps a submitted flag and
returns an empty error object until it is set.
Clear an error the moment its field is edited:
const setField = useCallback((field: keyof CheckoutFormValues, value: string) => {
setValues((current) => ({ ...current, [field]: value }));
setErrors((current) => {
if (!current[field]) return current;
const next = { ...current };
delete next[field];
return next;
});
}, []);Note the early return current. Returning the same object when nothing changed means
React skips the re-render — a small thing that fires on every keystroke.
The keyboard covering the form
Covered in lesson 6, and it belongs on the checklist here too: the screen needs
KeyboardAvoidingView with a per-platform behavior, and
keyboardShouldPersistTaps="handled" so the first tap on your submit button is not eaten
dismissing the keyboard.
Libraries
React Hook Form works in React Native and is a good choice for large forms — twenty fields, cross-field rules, dynamic arrays. For a checkout with seven, the version above is less code and nothing to learn. Start plain; reach for the library when the plain version starts hurting.
What is next
Modals, Sheets and Overlays — real native
windows, and why createPortal has no equivalent here.