React Native Form Validation with Native Tools, Formik, Yup, and Hooks
Build robust React Native forms with native inputs, Formik state, Yup schemas, hooks, async checks, accessibility, and production troubleshooting.
React Native does not submit an HTML form. Connect each native TextInput to a Formik field, validate with a Yup object schema, and call Formik’s handleSubmit from a native Button. This pattern keeps values, touched state, errors, and asynchronous submission in one place while preserving native controls.
1. Install and define the form contract
Add Formik and Yup to your React Native project. Keep the schema close to the feature so constraints and messages can be reviewed with product requirements.
npm install formik yup
# or
yarn add formik yup
Use one initialValues key per field. The keys must match the names passed to handleChange, handleBlur, and the Yup schema.
2. Complete native Formik and Yup example
This screen uses native TextInput, Button, Text, and View components. There is no web <form>, <Field>, or DOM markup.
import React from 'react';
import { Alert, Button, StyleSheet, Text, TextInput, View } from 'react-native';
import { Formik } from 'formik';
import * as Yup from 'yup';
const validationSchema = Yup.object({
name: Yup.string().trim('Remove leading and trailing spaces').min(2, 'Use at least 2 characters').max(80, 'Use 80 characters or fewer').required('Name is required'),
email: Yup.string().trim().email('Enter a valid email address').required('Email is required'),
password: Yup.string().min(8, 'Use at least 8 characters').required('Password is required'),
});
export default function SignUpScreen() {
return (
<Formik
initialValues={{ name: '', email: '', password: '' }}
validationSchema={validationSchema}
onSubmit={async (values, { setSubmitting, setStatus, resetForm }) => {
setStatus(undefined);
try {
await new Promise(resolve => setTimeout(resolve, 300));
Alert.alert('Account created');
resetForm();
} catch (error) {
setStatus('The server could not create your account. Try again.');
} finally {
setSubmitting(false);
}
}}
>
{({ values, errors, touched, handleChange, handleBlur, handleSubmit, isSubmitting, status }) => (
<View style={styles.container}>
<Text style={styles.label}>Name</Text>
<TextInput style={styles.input} value={values.name} onChangeText={handleChange('name')} onBlur={handleBlur('name')} autoCapitalize='words' returnKeyType='next' accessibilityLabel='Name' />
{touched.name && errors.name ? <Text style={styles.error}>{errors.name}</Text> : null}
<Text style={styles.label}>Email</Text>
<TextInput style={styles.input} value={values.email} onChangeText={handleChange('email')} onBlur={handleBlur('email')} keyboardType='email-address' autoCapitalize='none' autoCorrect={false} returnKeyType='next' accessibilityLabel='Email' />
{touched.email && errors.email ? <Text style={styles.error}>{errors.email}</Text> : null}
<Text style={styles.label}>Password</Text>
<TextInput style={styles.input} value={values.password} onChangeText={handleChange('password')} onBlur={handleBlur('password')} secureTextEntry autoCapitalize='none' autoCorrect={false} accessibilityLabel='Password' />
{touched.password && errors.password ? <Text style={styles.error}>{errors.password}</Text> : null}
{status ? <Text style={styles.error}>{status}</Text> : null}
<Button title={isSubmitting ? 'Submitting…' : 'Submit'} onPress={handleSubmit} disabled={isSubmitting} />
</View>
)}
</Formik>
);
}
const styles = StyleSheet.create({ container: { padding: 24, gap: 8 }, label: { fontWeight: '600', marginTop: 12 }, input: { borderWidth: 1, borderColor: '#999', borderRadius: 6, padding: 10 }, error: { color: '#b00020' } });
Formik’s React Native guide passes handleSubmit to Button onPress because React Native has no <form onSubmit> element. Its native example maps initialValues, values, handleChange(fieldName), and handleBlur(fieldName) to each TextInput.
3. Understand native event mapping
| Native event | Formik connection | Purpose |
|---|---|---|
onChangeText |
handleChange('email') or setFieldValue |
Store the new value. |
onBlur |
handleBlur('email') or setFieldTouched |
Mark the field touched and trigger blur validation. |
onSubmitEditing |
Move focus or call handleSubmit |
Handle the keyboard return action. |
Button onPress |
handleSubmit |
Validate, then invoke onSubmit when valid. |
React Native’s input documentation notes that text can be validated as the user types. Immediate validation is optional.
4. Control validation timing and errors
Formik validates after changes and blurs by default. Configure this deliberately:
<Formik validateOnChange={false} validateOnBlur={true} validateOnMount={false} ... />
- Change plus blur: useful for short forms.
- Blur only: reduces noise while typing.
- Submit focused: disable change validation and reveal errors after submission.
Render an error only when both touched[field] and errors[field] exist. Use setStatus for form-level server failures and setFieldError for field-specific responses.
5. Compose fields with Formik hooks
useField, useFormik, and useFormikContext change state composition, not native input events.
import React from 'react';
import { Text, TextInput, View } from 'react-native';
import { useField } from 'formik';
export function FormikTextField({ name, label, ...inputProps }) {
const [field, meta, helpers] = useField(name);
return (
<View>
<Text>{label}</Text>
<TextInput {...inputProps} value={field.value == null ? '' : String(field.value)} onChangeText={text => helpers.setValue(text)} onBlur={() => helpers.setTouched(true)} />
{meta.touched && meta.error ? <Text style={{ color: 'red' }}>{meta.error}</Text> : null}
</View>
);
}
Use useFormikContext for cross-field behavior and useFormik when a component needs explicit form props. You still connect returned handlers to native controls.
6. Custom and asynchronous validation
Formik accepts synchronous and asynchronous field-level or form-level validators.
const validate = async values => {
const errors = {};
if (!values.username) errors.username = 'Username is required';
if (values.password !== values.confirmPassword) errors.confirmPassword = 'Passwords must match';
if (values.username) {
const available = await checkUsername(values.username);
if (!available) errors.username = 'That username is already taken';
}
return errors;
};
Debounce availability checks or cancel stale requests so an older response cannot replace a newer result. Keep deterministic rules in Yup and reserve asynchronous validation for server-dependent checks.
7. Keyboard, focus, and accessibility
- Use
keyboardType='email-address',autoCapitalize='none', andautoCorrect={false}for email fields. - Use
secureTextEntryfor passwords and never log them. - Wire
onSubmitEditingto focus the next input or submit the final one. - Provide accessibility labels and make errors readable without relying on color.
- Normalize nullable initial data to empty strings to avoid controlled-input warnings.
8. Edge cases and safeguards
- Disable the Button while
isSubmittingto prevent double taps. - Keyboard submission does not automatically call Formik; wire it explicitly.
- Cancel in-flight requests or ignore results after unmount.
- Map API field names to
setFieldError. - For dynamic fields, keep stable keys and validate arrays with Yup.
- Use
setFieldValuefor booleans, numbers, dates, and picker values. - Repeat all security and business rules on the server.
9. Testing and debugging checklist
- Submit empty and partially complete forms.
- Check every minimum, maximum, and email boundary.
- Verify blur and touched behavior.
- Test slow, failed, and retried requests.
- Test screen-reader and keyboard flows.
- Confirm payload normalization and that passwords are never logged.
10. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Input does not update | Wrong key or missing value binding. | Align initialValues, value, and handleChange. |
| Error never appears | Field is not touched. | Call handleBlur(name) and check touched plus error. |
| Button does nothing | Submit handler not connected or validation fails. | Use onPress={handleSubmit} and inspect errors. |
| Unexpected Yup shape | Schema paths differ from values. | Align object paths and render nested errors correctly. |
| Old async result wins | Requests finish out of order. | Debounce, cancel, or ignore stale responses. |
| Form submits twice | Rapid presses or duplicate keyboard/Button handlers. | Guard with isSubmitting. |
11. Performance, reliability, and cost
Validation is usually cheap; asynchronous checks, large schemas, and unnecessary rerenders cost more. Keep field components small, avoid recreating heavy schemas, debounce remote checks, and validate only the current step. Always restore setSubmitting(false) in finally. Client validation improves UX but does not replace server validation, retries, timeouts, or secure transport.
12. Or skip the browser setup
If your workflow also needs screenshots for QA, documentation, or an AI agent, ScreenshotNeo returns an image or PDF from one GET request. It removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify verdict and billing with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API docs for options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo supports full-page and element captures, dark mode, device presets, retina scale, custom CSS and JavaScript, waits, request blocking, headers and cookies, geolocation, PDFs, caching, signed links, async jobs, bulk capture, and a usage API. Every feature is on every plan: 1,000 shots a month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
13. FAQ
Should I validate on every keystroke?
Only when immediate feedback helps. Blur or submit validation is calmer for passwords and longer fields.
Can Yup check a value against my API?
Use an asynchronous Formik validator or submit handler, debounce it, and handle stale responses.
Do hooks replace native event handlers?
No. Hooks expose state and helpers; you still connect onChangeText, onBlur, and submit actions.
Is client validation enough?
No. Repeat security and business rules on the server.


