Forms Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Control every TextInput: Pair value with onChangeText - uncontrolled inputs fight autofill, reset logic, and validation libraries on mobile.
Match keyboardType to the field: email-address, phone-pad, and numeric reduce errors and speed entry - wrong keyboards frustrate users on small screens.
Chain fields with returnKeyType: Use next + onSubmitEditing → .focus() on the next ref - fewer taps than dismissing the keyboard between fields.
Set blurOnSubmit false on multi-field screens: Last field uses done - intermediate fields should move focus, not dismiss the keyboard prematurely.
Dismiss keyboard on submit: Call Keyboard.dismiss() in the submit handler so success states and navigation are not obscured.
Use keyboardShouldPersistTaps on scroll forms: ScrollView/FlatList wrappers need keyboardShouldPersistTaps="handled" so submit buttons receive taps while the keyboard is open.
Prefer react-hook-form on mobile: Subscriptions beat giant useState forms - wire inputs with Controller, not web-style register.
Validate on touch, not every keystroke: mode: "onTouched" keeps inline errors calm - screaming validation on onChange feels hostile on phones.
Single source of truth with Zod: Export z.infer<typeof Schema> - duplicating interfaces beside schemas drifts on the first API change.
Coerce TextInput strings in schemas: z.coerce.number() or explicit transforms - TextInput always delivers strings even for numeric fields.
Put cross-field errors on the right path: Password confirm .refine() needs path: ["confirmPassword"] - root errors are invisible on mobile.
Map server validation with setError: HTTP 422 field errors should land on the matching Controller - generic toasts alone force users to guess what failed.
Install KeyboardProvider at the root: react-native-keyboard-controller requires the provider in _layout - without it, aware scroll and toolbar helpers noop.
Prefer KeyboardAwareScrollView over naive KAV: Keyboard controller tracks focused inputs more reliably across iOS and Android edge-to-edge (RN 0.86).
Set Android softwareKeyboardLayoutMode: app.config softwareKeyboardLayoutMode: "pan" pairs with keyboard-controller - resizes behave differently per OEM without it.
Use native pickers for dates when UX allows: @react-native-community/datetimepicker respects platform conventions - custom wheels cost accessibility and maintenance.
Store picker values as Date or ISO in state: Format with Intl at the display edge - parsing locale strings in submit handlers breaks on i18n.
One form object across wizard steps: FormProvider holds all fields - per-step useState loses email when step 2 mounts.
Validate only current step fields on Next: trigger(STEP_FIELDS[step]) - full-schema validation on step 1 fails because step 3 is empty.
Persist wizard drafts: MMKV or AsyncStorage autosave survives process death - confirm before Cancel when isDirty.
Intercept Android hardware back in wizards: BackHandler decrements step index - otherwise users leave the flow accidentally.
Label every field for accessibility: accessibilityLabel or visible labels linked with accessibilityLabelledBy - placeholder text is not a label.
Announce form errors: AccessibilityInfo.announceForAccessibility on submit failure - screen readers may not move focus to the first error automatically.
Focus the first invalid field after submit: setFocus on the first error path - mobile users should not hunt for red borders off-screen.
Queue offline submissions explicitly: Optimistic UI plus a persisted retry queue - silent failure offline is worse than a clear "Saved locally" state.
useState is fine for a single screen with three fields and no server errors.react-native-keyboard-controller for consistent inset handling on RN 0.86.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026