Validation Timing
- Validate on blur for better UX, not on every keystroke
- Show success states only after user interaction
- Use warning states for non-critical issues
FormControl provides a complete solution for form field management with built-in validation states, accessibility features, and contextual styling. It includes label, helper text, and error/success/warning message components with automatic state management.
import { FormControl, FormControlLabel, FormControlLabelText, FormControlHelper, FormControlHelperText, FormControlError, FormControlErrorIcon, FormControlErrorText, FormControlSuccess, FormControlSuccessIcon, FormControlSuccessText, FormControlWarning, FormControlWarningIcon, FormControlWarningText, useFormControl, useFormControlOptional,} from 'rnc-theme';
<FormControl> <FormControlLabel> <FormControlLabelText>Email Address</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Enter your email" /> <FormControlHelper> <FormControlHelperText>We'll never share your email</FormControlHelperText> </FormControlHelper></FormControl>
<FormControl required> <FormControlLabel> <FormControlLabelText>Full Name</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Enter your full name" /></FormControl>
<FormControl state="error"> <FormControlLabel> <FormControlLabelText>Password</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Enter password" secureTextEntry /> <FormControlError> <FormControlErrorIcon /> <FormControlErrorText>Password must be at least 8 characters</FormControlErrorText> </FormControlError></FormControl>
<FormControl state="success"> <FormControlLabel> <FormControlLabelText>Username</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Choose username" /> <FormControlSuccess> <FormControlSuccessIcon /> <FormControlSuccessText>Username is available!</FormControlSuccessText> </FormControlSuccess></FormControl>
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | Form control content |
id | string | - | Unique identifier (auto-generated if not provided) |
state | FormControlState | 'default' | Visual state (default, error, success, warning, disabled) |
size | FormControlSize | 'md' | Size variant (sm, md, lg) |
disabled | boolean | false | Disable all form control elements |
required | boolean | false | Mark field as required |
style | StyleProp<ViewStyle> | - | Additional container styles |
spacing | keyof Theme['spacing'] | 'sm' | Spacing between child elements |
onStateChange | (state: FormControlState) => void | - | Callback when state changes |
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | Label content |
style | StyleProp<ViewStyle> | - | Additional label container styles |
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | Label text content |
style | TextStyle | - | Additional text styles |
variant | keyof Theme['typography'] | - | Typography variant override |
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | Helper content |
style | StyleProp<ViewStyle> | - | Additional helper container styles |
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | Helper text content |
style | TextStyle | - | Additional text styles |
variant | keyof Theme['typography'] | - | Typography variant override |
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | Message content |
style | StyleProp<ViewStyle> | - | Additional container styles |
showWhen | boolean | - | Control visibility manually |
Prop | Type | Default | Description |
---|---|---|---|
icon | React.ReactNode | - | Custom icon component |
style | StyleProp<ViewStyle> | - | Additional icon container styles |
size | number | 16 | Icon size in pixels |
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | - | Text content |
style | TextStyle | - | Additional text styles |
variant | keyof Theme['typography'] | - | Typography variant override |
State | Description | Use Case |
---|---|---|
default | Normal state | Standard form fields |
error | Error state | Validation failures |
success | Success state | Successful validation |
warning | Warning state | Caution messages |
disabled | Disabled state | Inactive form fields |
const LoginForm = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [emailError, setEmailError] = useState(''); const [passwordError, setPasswordError] = useState('');
const validateEmail = (value: string) => { if (!value) { setEmailError('Email is required'); return false; } if (!/\S+@\S+\.\S+/.test(value)) { setEmailError('Please enter a valid email'); return false; } setEmailError(''); return true; };
const validatePassword = (value: string) => { if (!value) { setPasswordError('Password is required'); return false; } if (value.length < 8) { setPasswordError('Password must be at least 8 characters'); return false; } setPasswordError(''); return true; };
return ( <VStack spacing="lg" padding="xl"> <FormControl state={emailError ? 'error' : email ? 'success' : 'default'} required > <FormControlLabel> <FormControlLabelText>Email Address</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Enter your email" value={email} onChangeText={(value) => { setEmail(value); validateEmail(value); }} keyboardType="email-address" autoCapitalize="none" /> {emailError ? ( <FormControlError> <FormControlErrorIcon /> <FormControlErrorText>{emailError}</FormControlErrorText> </FormControlError> ) : email ? ( <FormControlSuccess> <FormControlSuccessIcon /> <FormControlSuccessText>Valid email address</FormControlSuccessText> </FormControlSuccess> ) : ( <FormControlHelper> <FormControlHelperText>We'll never share your email</FormControlHelperText> </FormControlHelper> )} </FormControl>
<FormControl state={passwordError ? 'error' : password.length >= 8 ? 'success' : 'default'} required > <FormControlLabel> <FormControlLabelText>Password</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Enter your password" value={password} onChangeText={(value) => { setPassword(value); validatePassword(value); }} secureTextEntry /> {passwordError ? ( <FormControlError> <FormControlErrorIcon /> <FormControlErrorText>{passwordError}</FormControlErrorText> </FormControlError> ) : password.length >= 8 ? ( <FormControlSuccess> <FormControlSuccessIcon /> <FormControlSuccessText>Strong password</FormControlSuccessText> </FormControlSuccess> ) : ( <FormControlHelper> <FormControlHelperText>Password must be at least 8 characters</FormControlHelperText> </FormControlHelper> )} </FormControl> </VStack> );};
const RegistrationForm = () => { const [formData, setFormData] = useState({ username: '', email: '', password: '', confirmPassword: '', terms: false, }); const [validationState, setValidationState] = useState({});
const validateField = (field: string, value: any) => { let isValid = true; let message = '';
switch (field) { case 'username': if (!value) { isValid = false; message = 'Username is required'; } else if (value.length < 3) { isValid = false; message = 'Username must be at least 3 characters'; } else if (!/^[a-zA-Z0-9_]+$/.test(value)) { isValid = false; message = 'Username can only contain letters, numbers, and underscores'; } break;
case 'email': if (!value) { isValid = false; message = 'Email is required'; } else if (!/\S+@\S+\.\S+/.test(value)) { isValid = false; message = 'Please enter a valid email address'; } break;
case 'password': if (!value) { isValid = false; message = 'Password is required'; } else if (value.length < 8) { isValid = false; message = 'Password must be at least 8 characters'; } else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) { isValid = false; message = 'Password must contain uppercase, lowercase, and number'; } break;
case 'confirmPassword': if (!value) { isValid = false; message = 'Please confirm your password'; } else if (value !== formData.password) { isValid = false; message = 'Passwords do not match'; } break; }
setValidationState(prev => ({ ...prev, [field]: { isValid, message } }));
return isValid; };
const getFieldState = (field: string) => { const validation = validationState[field]; if (!validation) return 'default'; if (!formData[field]) return 'default'; return validation.isValid ? 'success' : 'error'; };
return ( <VStack spacing="lg" padding="xl"> <FormControl state={getFieldState('username')} required size="md" > <FormControlLabel> <FormControlLabelText>Username</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Choose a username" value={formData.username} onChangeText={(value) => { setFormData(prev => ({ ...prev, username: value })); validateField('username', value); }} autoCapitalize="none" /> {validationState.username?.message ? ( getFieldState('username') === 'error' ? ( <FormControlError> <FormControlErrorIcon /> <FormControlErrorText>{validationState.username.message}</FormControlErrorText> </FormControlError> ) : ( <FormControlSuccess> <FormControlSuccessIcon /> <FormControlSuccessText>Username is available</FormControlSuccessText> </FormControlSuccess> ) ) : ( <FormControlHelper> <FormControlHelperText>Choose a unique username (3+ characters)</FormControlHelperText> </FormControlHelper> )} </FormControl>
<FormControl state={getFieldState('email')} required > <FormControlLabel> <FormControlLabelText>Email Address</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Enter your email" value={formData.email} onChangeText={(value) => { setFormData(prev => ({ ...prev, email: value })); validateField('email', value); }} keyboardType="email-address" autoCapitalize="none" /> {validationState.email?.message && getFieldState('email') === 'error' && ( <FormControlError> <FormControlErrorIcon /> <FormControlErrorText>{validationState.email.message}</FormControlErrorText> </FormControlError> )} </FormControl>
<FormControl state={getFieldState('password')} required > <FormControlLabel> <FormControlLabelText>Password</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Create a password" value={formData.password} onChangeText={(value) => { setFormData(prev => ({ ...prev, password: value })); validateField('password', value); if (formData.confirmPassword) { validateField('confirmPassword', formData.confirmPassword); } }} secureTextEntry /> {validationState.password?.message ? ( getFieldState('password') === 'error' ? ( <FormControlError> <FormControlErrorIcon /> <FormControlErrorText>{validationState.password.message}</FormControlErrorText> </FormControlError> ) : ( <FormControlSuccess> <FormControlSuccessIcon /> <FormControlSuccessText>Strong password</FormControlSuccessText> </FormControlSuccess> ) ) : ( <FormControlHelper> <FormControlHelperText> Include uppercase, lowercase, and numbers (8+ characters) </FormControlHelperText> </FormControlHelper> )} </FormControl>
<FormControl state={getFieldState('confirmPassword')} required > <FormControlLabel> <FormControlLabelText>Confirm Password</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Confirm your password" value={formData.confirmPassword} onChangeText={(value) => { setFormData(prev => ({ ...prev, confirmPassword: value })); validateField('confirmPassword', value); }} secureTextEntry /> {validationState.confirmPassword?.message && getFieldState('confirmPassword') === 'error' && ( <FormControlError> <FormControlErrorIcon /> <FormControlErrorText>{validationState.confirmPassword.message}</FormControlErrorText> </FormControlError> )} </FormControl> </VStack> );};
<VStack spacing="xl" padding="lg"> <FormControl size="sm"> <FormControlLabel> <FormControlLabelText>Small Size</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Small input" /> <FormControlHelper> <FormControlHelperText>Small helper text</FormControlHelperText> </FormControlHelper> </FormControl>
<FormControl size="md"> <FormControlLabel> <FormControlLabelText>Medium Size (Default)</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Medium input" /> <FormControlHelper> <FormControlHelperText>Medium helper text</FormControlHelperText> </FormControlHelper> </FormControl>
<FormControl size="lg"> <FormControlLabel> <FormControlLabelText>Large Size</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Large input" /> <FormControlHelper> <FormControlHelperText>Large helper text</FormControlHelperText> </FormControlHelper> </FormControl></VStack>
const PasswordChangeForm = () => { const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const isWeakPassword = (password: string) => { return password.length >= 6 && password.length < 8; };
return ( <VStack spacing="lg"> <FormControl> <FormControlLabel> <FormControlLabelText>Current Password</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Enter current password" value={currentPassword} onChangeText={setCurrentPassword} secureTextEntry /> </FormControl>
<FormControl state={ newPassword && isWeakPassword(newPassword) ? 'warning' : newPassword && newPassword.length >= 8 ? 'success' : 'default' } required > <FormControlLabel> <FormControlLabelText>New Password</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Enter new password" value={newPassword} onChangeText={setNewPassword} secureTextEntry /> {isWeakPassword(newPassword) ? ( <FormControlWarning> <FormControlWarningIcon /> <FormControlWarningText> Password is acceptable but could be stronger </FormControlWarningText> </FormControlWarning> ) : newPassword.length >= 8 ? ( <FormControlSuccess> <FormControlSuccessIcon /> <FormControlSuccessText>Strong password!</FormControlSuccessText> </FormControlSuccess> ) : ( <FormControlHelper> <FormControlHelperText> Minimum 8 characters recommended </FormControlHelperText> </FormControlHelper> )} </FormControl>
<FormControl state={ confirmPassword && confirmPassword !== newPassword ? 'error' : confirmPassword && confirmPassword === newPassword ? 'success' : 'default' } required > <FormControlLabel> <FormControlLabelText>Confirm New Password</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Confirm new password" value={confirmPassword} onChangeText={setConfirmPassword} secureTextEntry /> {confirmPassword && confirmPassword !== newPassword && ( <FormControlError> <FormControlErrorIcon /> <FormControlErrorText>Passwords do not match</FormControlErrorText> </FormControlError> )} {confirmPassword && confirmPassword === newPassword && ( <FormControlSuccess> <FormControlSuccessIcon /> <FormControlSuccessText>Passwords match</FormControlSuccessText> </FormControlSuccess> )} </FormControl> </VStack> );};
const useFormValidation = (initialValues: Record<string, any>) => { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState<Record<string, string>>({}); const [touched, setTouched] = useState<Record<string, boolean>>({});
const setValue = (field: string, value: any) => { setValues(prev => ({ ...prev, [field]: value })); if (touched[field]) { validateField(field, value); } };
const setTouched = (field: string) => { setTouched(prev => ({ ...prev, [field]: true })); validateField(field, values[field]); };
const validateField = (field: string, value: any) => { // Add your validation logic here // This is a simplified example let error = '';
if (field === 'email' && value && !/\S+@\S+\.\S+/.test(value)) { error = 'Invalid email format'; }
setErrors(prev => ({ ...prev, [field]: error })); return !error; };
const getFieldProps = (field: string) => ({ value: values[field] || '', onChangeText: (value: string) => setValue(field, value), onBlur: () => setTouched(field), });
const getFormControlProps = (field: string) => ({ state: errors[field] ? 'error' as const : values[field] && touched[field] ? 'success' as const : 'default' as const, });
return { values, errors, touched, getFieldProps, getFormControlProps, validateField, setTouched, };};
// Usageconst MyForm = () => { const { getFieldProps, getFormControlProps, errors } = useFormValidation({ email: '', password: '', });
return ( <VStack spacing="md"> <FormControl {...getFormControlProps('email')} required> <FormControlLabel> <FormControlLabelText>Email</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Enter email" {...getFieldProps('email')} keyboardType="email-address" /> {errors.email && ( <FormControlError> <FormControlErrorIcon /> <FormControlErrorText>{errors.email}</FormControlErrorText> </FormControlError> )} </FormControl>
<FormControl {...getFormControlProps('password')} required> <FormControlLabel> <FormControlLabelText>Password</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Enter password" {...getFieldProps('password')} secureTextEntry /> {errors.password && ( <FormControlError> <FormControlErrorIcon /> <FormControlErrorText>{errors.password}</FormControlErrorText> </FormControlError> )} </FormControl> </VStack> );};
const DynamicFormControl = () => { const [fieldState, setFieldState] = useState<FormControlState>('default'); const [inputValue, setInputValue] = useState('');
// Simulate real-time validation useEffect(() => { const timer = setTimeout(() => { if (!inputValue) { setFieldState('default'); } else if (inputValue.length < 3) { setFieldState('error'); } else if (inputValue.length < 6) { setFieldState('warning'); } else { setFieldState('success'); } }, 500);
return () => clearTimeout(timer); }, [inputValue]);
return ( <FormControl state={fieldState} onStateChange={(newState) => { console.log('State changed to:', newState); }} > <FormControlLabel> <FormControlLabelText>Dynamic Validation</FormControlLabelText> </FormControlLabel> <TextInput placeholder="Type to see validation states" value={inputValue} onChangeText={setInputValue} />
<FormControlError showWhen={fieldState === 'error'}> <FormControlErrorIcon /> <FormControlErrorText>Too short (minimum 3 characters)</FormControlErrorText> </FormControlError>
<FormControlWarning showWhen={fieldState === 'warning'}> <FormControlWarningIcon /> <FormControlWarningText>Could be longer (6+ recommended)</FormControlWarningText> </FormControlWarning>
<FormControlSuccess showWhen={fieldState === 'success'}> <FormControlSuccessIcon /> <FormControlSuccessText>Perfect length!</FormControlSuccessText> </FormControlSuccess>
<FormControlHelper> <FormControlHelperText> {fieldState === 'default' && 'Start typing to see validation'} </FormControlHelperText> </FormControlHelper> </FormControl> );};
Validation Timing
Message Hierarchy
Performance
useFormControlOptional
for optional form control context