Forms
Forms are a common feature of any application. This section shows you how to handle forms the right way with this React Native template using react-hook-form
and custom form components.
React Hook Form Integration
This template uses react-hook-form
to handle forms
Make sure to check the react-hook-form documentation to learn more about how to use it.
Form Components Overview
The template provides a set of controlled components that are specifically designed to work with react-hook-form
. All form components are located in src/components/form/
and follow a consistent API pattern:
- FTextInput: Text input field with validation
- FTextInputBottomSheet: Text input optimized for bottom sheet usage
- FSelectSingle: Single selection dropdown with search functionality
- FSelectChip: Multi/single selection using chip components
- FSwitch: Toggle switch component
Basic Form Setup
Here's how to set up a basic form using the template's form components, as demonstrated in
import React, { useCallback, useMemo } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import FTextInput from "@/components/form/form.textInput";
import BButton from "@/components/base/base.button";
import BView from "@/components/base/base.view";
type FormData = {
email: string;
password: string;
};
const MyForm = () => {
const { control, handleSubmit } = useForm<FormData>();
const ruleInput = useMemo(
() => ({
email: {
required: {
value: true,
message: "Email should not be empty",
},
pattern: {
value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
message: "Email is not valid",
},
},
password: {
required: {
value: true,
message: "Password should not be empty",
},
},
}),
[]
);
const onSubmit: SubmitHandler<FormData> = useCallback(async (data) => {
console.log("Form data:", data);
// Handle form submission
}, []);
return (
<BView gap="md">
<FTextInput
name="email"
control={control}
placeholder="Your email"
rules={ruleInput.email}
leftIcon="email"
keyboardType="email-address"
/>
<FTextInput
name="password"
control={control}
placeholder="Your password"
rules={ruleInput.password}
secureTextEntry
leftIcon="lock"
/>
<BButton onPress={handleSubmit(onSubmit)} label="Submit" />
</BView>
);
};
Components
FTextInput Component
The react-hook-form
.
Props Interface
type FTextInputProps = BTextInputProps & {
control: Control<any>; // React Hook Form control
name: string; // Field name
defaultValue?: string; // Default value
hint?: string; // Helper text
containerStyle?: StyleProp<ViewStyle>; // Container styling
rules?: RegisterOptions; // Validation rules
};
Usage Examples
// Basic text input
<FTextInput
name="username"
control={control}
placeholder="Enter username"
rules={{
required: { value: true, message: "Username is required" },
minLength: { value: 3, message: "Minimum 3 characters" }
}}
/>
// Email input with validation
<FTextInput
name="email"
control={control}
placeholder="Enter email"
keyboardType="email-address"
leftIcon="email"
rules={{
required: { value: true, message: "Email is required" },
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "Invalid email format"
}
}}
/>
// Password input
<FTextInput
name="password"
control={control}
placeholder="Enter password"
secureTextEntry
leftIcon="lock"
rules={{
required: { value: true, message: "Password is required" },
minLength: { value: 8, message: "Minimum 8 characters" }
}}
/>
// Text input with hint
<FTextInput
name="description"
control={control}
placeholder="Enter description"
multiline
numberOfLines={4}
hint="Provide a brief description (optional)"
/>
FTextInputBottomSheet Component
The
Usage
import FTextInputBottomSheet from "@/components/form/form.textInput.bottomSheet";
// Inside a bottom sheet modal
<FTextInputBottomSheet
name="comment"
control={control}
placeholder="Add your comment"
multiline
numberOfLines={3}
rules={{
required: { value: true, message: "Comment is required" },
}}
/>;
FSelectSingle Component
The
Props Interface
type FSelectSingleProps = BTextInputProps & {
control: Control<any>;
name: string;
defaultValue?: { value: any; label?: string };
hint?: string;
label?: string;
rules?: RegisterOptions;
search?: boolean; // Enable search functionality
disabled?: boolean;
searchPlaceHolder?: string;
onChangeSearchValue?: (value: string) => Promise<FSelectSingleItem[]>;
heightSelectBox?: string | number;
initData: FSelectSingleItem[]; // Options data
onValueChange?: (value: FSelectSingleItem) => void;
renderItem?: (item, currentValue, onPress) => React.JSX.Element;
};
type FSelectSingleItem = {
value: any;
label: string;
[key: string]: any;
};
Usage Examples
// Basic select
const countryOptions = [
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'ca', label: 'Canada' },
];
<FSelectSingle
name="country"
control={control}
label="Select Country"
placeholder="Choose a country"
initData={countryOptions}
rules={{
required: { value: true, message: "Country is required" }
}}
/>
// Select with search
<FSelectSingle
name="city"
control={control}
label="Select City"
placeholder="Choose a city"
search={true}
searchPlaceHolder="Search cities..."
initData={cityOptions}
heightSelectBox="60%"
onValueChange={(selected) => {
console.log('Selected city:', selected);
}}
/>
// Select with async search
<FSelectSingle
name="user"
control={control}
label="Select User"
placeholder="Choose a user"
search={true}
initData={userOptions}
onChangeSearchValue={async (searchTerm) => {
const response = await searchUsers(searchTerm);
return response.data;
}}
/>
// Custom item rendering
<FSelectSingle
name="product"
control={control}
label="Select Product"
initData={productOptions}
renderItem={(item, currentValue, onPress) => (
<BPressable
onPress={() => onPress(item)}
paddingVertical="md"
flexDirection="row"
alignItems="center"
gap="sm"
>
<BImage source={{ uri: item.image }} width={40} height={40} />
<BView flex={1}>
<BText variant="md" fontWeight="bold">{item.label}</BText>
<BText variant="sm" color="secondary">{item.description}</BText>
</BView>
{currentValue?.value === item.value && (
<BIcon name="check" color="primary" />
)}
</BPressable>
)}
/>
FSelectChip Component
The
Props Interface
type FSelectChipProps = {
control: Control<any>;
name: string;
defaultValue?: FChipItem[];
hint?: string;
style?: StyleProp<ViewStyle>;
containerStyle?: StyleProp<ViewStyle>;
rules?: RegisterOptions;
initData: FChipItem[]; // Chip options
onValueChange?: (values: FChipItem[]) => void;
multi?: boolean; // Enable multiple selection
chipProps?: BChipProps; // Props for chip styling
};
type FChipItem = {
value: any;
label: string;
icon?: string;
};
Usage Examples
// Single selection chips
const sizeOptions = [
{ value: 'xs', label: 'Extra Small' },
{ value: 's', label: 'Small' },
{ value: 'm', label: 'Medium' },
{ value: 'l', label: 'Large' },
{ value: 'xl', label: 'Extra Large' },
];
<FSelectChip
name="size"
control={control}
initData={sizeOptions}
multi={false}
hint="Select your preferred size"
rules={{
required: { value: true, message: "Size is required" }
}}
/>
// Multiple selection chips
const interestOptions = [
{ value: 'tech', label: 'Technology', icon: 'laptop' },
{ value: 'sports', label: 'Sports', icon: 'football' },
{ value: 'music', label: 'Music', icon: 'music' },
{ value: 'travel', label: 'Travel', icon: 'airplane' },
];
<FSelectChip
name="interests"
control={control}
initData={interestOptions}
multi={true}
chipProps={{
size: 'md',
variant: 'outline'
}}
containerStyle={{
flexWrap: 'wrap',
gap: 8
}}
onValueChange={(selected) => {
console.log('Selected interests:', selected);
}}
/>
// Custom styled chips
<FSelectChip
name="tags"
control={control}
initData={tagOptions}
multi={true}
chipProps={{
backgroundColor: 'primary',
labelColor: 'white',
selectedBackgroundColor: 'secondary',
borderRadius: 'full'
}}
/>
FSwitch Component
The
Props Interface
type FSwitchProps = SwitchProps & {
control: Control<any>;
name: string;
defaultValue?: boolean;
hint?: string;
containerStyle?: StyleProp<ViewStyle>;
rules?: RegisterOptions;
};
Usage Examples
// Basic switch
<FSwitch
name="notifications"
control={control}
defaultValue={true}
hint="Enable push notifications"
/>
// Switch with validation
<FSwitch
name="terms"
control={control}
defaultValue={false}
rules={{
required: {
value: true,
message: "You must accept the terms and conditions"
}
}}
onValueChange={(value) => {
console.log('Terms accepted:', value);
}}
/>
// Custom styled switch
<BView flexDirection="row" alignItems="center" justifyContent="space-between">
<BView flex={1}>
<BText variant="md" fontWeight="bold">Dark Mode</BText>
<BText variant="sm" color="secondary">Switch to dark theme</BText>
</BView>
<FSwitch
name="darkMode"
control={control}
trackColor={{ false: '#767577', true: '#81b0ff' }}
thumbColor={watchDarkMode ? '#f5dd4b' : '#f4f3f4'}
/>
</BView>
Form Validation
All form components support react-hook-form
validation rules. Here are common validation patterns:
Common Validation Rules
const validationRules = {
// Required field
required: {
value: true,
message: "This field is required",
},
// Minimum length
minLength: {
value: 3,
message: "Minimum 3 characters required",
},
// Maximum length
maxLength: {
value: 50,
message: "Maximum 50 characters allowed",
},
// Pattern matching
pattern: {
value: /^[a-zA-Z0-9]+$/,
message: "Only alphanumeric characters allowed",
},
// Custom validation
validate: {
notEmpty: (value) => value.trim() !== "" || "Field cannot be empty",
uniqueEmail: async (value) => {
const exists = await checkEmailExists(value);
return !exists || "Email already exists";
},
},
};
Complex Validation Example
const passwordRules = {
required: {
value: true,
message: "Password is required",
},
minLength: {
value: 8,
message: "Password must be at least 8 characters",
},
validate: {
hasUpperCase: (value) =>
/[A-Z]/.test(value) ||
"Password must contain at least one uppercase letter",
hasLowerCase: (value) =>
/[a-z]/.test(value) ||
"Password must contain at least one lowercase letter",
hasNumber: (value) =>
/\d/.test(value) || "Password must contain at least one number",
hasSpecialChar: (value) =>
/[!@#$%^&*(),.?":{}|<>]/.test(value) ||
"Password must contain at least one special character",
},
};
Error Handling and Display
All form components automatically display validation errors. The error display follows this priority:
- Hint text: Shows when no validation errors exist
- Validation errors: Shows when field validation fails
- Error styling: Components automatically apply error styling when invalid
// Error display is automatic
<FTextInput
name="email"
control={control}
placeholder="Enter email"
hint="We'll never share your email" // Shows when valid
rules={{
required: { value: true, message: "Email is required" }, // Shows when invalid
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Invalid email" },
}}
/>
Best Practices
1. Form Structure
- Use
BKeyboardAvoidingView
for forms to handle keyboard properly - Group related fields using
BView
with appropriate spacing - Provide clear labels and placeholders
- Use appropriate keyboard types for different input types
2. Validation
- Define validation rules in
useMemo
to prevent unnecessary re-renders - Use descriptive error messages
- Implement client-side validation for better UX
- Consider server-side validation for security
3. Performance
- Use
defaultValues
inuseForm
to prevent controlled/uncontrolled warnings - Memoize validation rules and options
- Use
watch
sparingly to avoid unnecessary re-renders - Consider using
useController
for complex custom components
4. Accessibility
- Provide meaningful labels and hints
- Use appropriate
testID
props for testing - Ensure proper focus management
- Support screen readers with descriptive text
5. User Experience
- Show loading states during form submission
- Provide immediate feedback for validation errors
- Use appropriate input types and keyboards
- Implement proper error handling and user feedback
// Example of good form practices
const WellStructuredForm = () => {
const {
control,
handleSubmit,
formState: { isSubmitting },
} = useForm({
defaultValues: {
email: "",
password: "",
rememberMe: false,
},
});
const validationRules = useMemo(
() => ({
email: {
required: { value: true, message: "Email is required" },
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "Please enter a valid email",
},
},
password: {
required: { value: true, message: "Password is required" },
minLength: {
value: 6,
message: "Password must be at least 6 characters",
},
},
}),
[]
);
const onSubmit = useCallback(async (data) => {
try {
await submitForm(data);
showSuccessMessage("Form submitted successfully!");
} catch (error) {
showErrorMessage("Submission failed. Please try again.");
}
}, []);
return (
<BKeyboardAvoidingView flex={1} padding="lg">
<BView gap="md">
<FTextInput
testID="email-input"
name="email"
control={control}
placeholder="Enter your email"
keyboardType="email-address"
autoCapitalize="none"
leftIcon="email"
rules={validationRules.email}
/>
<FTextInput
testID="password-input"
name="password"
control={control}
placeholder="Enter your password"
secureTextEntry
leftIcon="lock"
rules={validationRules.password}
/>
<BView flexDirection="row" alignItems="center" gap="sm">
<FSwitch name="rememberMe" control={control} defaultValue={false} />
<BText>Remember me</BText>
</BView>
<BButton
testID="submit-button"
onPress={handleSubmit(onSubmit)}
label={isSubmitting ? "Submitting..." : "Submit"}
disabled={isSubmitting}
loading={isSubmitting}
/>
</BView>
</BKeyboardAvoidingView>
);
};
These form components provide a robust foundation for building forms in your React Native application while maintaining consistency with the design system and providing excellent user experience.