React Native – TextInput and Forms

July 1, 20264 min readUpdated 8/24/2026

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.