Building a Reusable React Native Input Component with React Hook Form


When developing forms in React Native applications, we frequently encounter these challenges:
Repetitive Code: Setting up input fields with proper styling and behavior for each form
Validation Complexity: Managing form validation and error states consistently
Customization Needs: Supporting various input accessories like icons and buttons
Type Safety: Ensuring form field names match the data structure
These challenges often lead to verbose, inconsistent code that's difficult to maintain. Let's explore how to build a reusable solution.
The Development Process
1. Identifying Requirements
Our component needs to:
Integrate with React Hook Form for validation and state management
Support standard React Native TextInput props
Allow for custom elements on either side of the input
Handle error display consistently
Maintain type safety with TypeScript
2. Planning the Interface
We'll design a TypeScript interface extending the standard TextInput props:
export interface RHFInputProps<T extends FieldValues> extends TextInputProps {
control: Control<T, any>; // React Hook Form control object
name: Path<T>; // Type-safe field name
left?: ReactNode; // Optional left accessory
right?: ReactNode; // Optional right accessory
}
The generic type parameter ensures that field names must exist in the form schema.
3. Integrating with React Hook Form
The core integration uses React Hook Form's Controller component to bridge between the form library and our UI component:
<Controller
control={control}
name={name}
render={({
field: { onChange, ...fieldProps },
fieldState: { error },
}) => (
// Render the input with validation state
)}
/>
This gives us access to:
The field's current value
Change handlers
Validation state and error messages
4. Designing the Component Structure
We need a layout that:
Positions the left and right accessories properly
Applies consistent styling
Displays error messages when validation fails
A nested View structure handles this well:
<View>
<View style={styles.input}>
{left}
<TextInput ... />
{right}
</View>
{error && <Text>{error.message}</Text>}
</View>
5. Styling for Consistency
We define styles outside the component to prevent recreation on each render:
const HEIGHT = 50;
const styles = StyleSheet.create({
input: {
height: HEIGHT,
borderRadius: design.radius,
borderWidth: 1,
borderColor: "gray",
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
gap: 12,
},
textInput: { flex: 1, height: HEIGHT, fontSize: fonts.size.md },
});
This approach uses design tokens to maintain consistency with the rest of the application.
The Final Component
Here's the complete implementation:
import React, { ReactNode } from "react";
import {
StyleSheet,
Text,
TextInput,
TextInputProps,
View,
} from "react-native";
import { Control, Controller, FieldValues, Path } from "react-hook-form";
import { design } from "@/theme";
import { fonts } from "@/fonts";
export interface RHFInputProps<T extends FieldValues> extends TextInputProps {
control: Control<T, any>;
name: Path<T>;
left?: ReactNode;
right?: ReactNode;
}
const RhfInput = <T extends FieldValues>({
control,
name,
left,
right,
...rest
}: RHFInputProps<T>) => {
return (
<Controller
control={control}
name={name}
render={({
field: { onChange, ...fieldProps },
fieldState: { error },
}) => (
<View>
<View style={styles.input}>
{left}
<TextInput
style={styles.textInput}
onChangeText={onChange}
{...fieldProps}
placeholderTextColor={"grey"}
{...rest}
/>
{right}
</View>
{error && <Text>{error.message}</Text>}
</View>
)}
/>
);
};
RhfInput.displayName = "RhfInput";
const HEIGHT = 50;
const styles = StyleSheet.create({
input: {
height: HEIGHT,
borderRadius: design.radius,
borderWidth: 1,
borderColor: "gray",
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
gap: 12,
},
textInput: { flex: 1, height: HEIGHT, fontSize: fonts.size.md },
});
export default RhfInput;
Usage Example
import React, { useState } from 'react';
import { View, StyleSheet, TouchableOpacity, Text, ScrollView } from 'react-native';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { Ionicons } from '@expo/vector-icons';
import RhfInput from './components/RhfInput'; // Import your custom component
// Define the form data structure with TypeScript
type SignupFormData = {
fullName: string;
email: string;
phone: string;
password: string;
confirmPassword: string;
};
// Create validation schema with Yup
const validationSchema = yup.object({
fullName: yup
.string()
.required('Full name is required')
.min(2, 'Name must be at least 2 characters'),
email: yup
.string()
.email('Please enter a valid email')
.required('Email is required'),
phone: yup
.string()
.matches(/^[0-9]{10}$/, 'Please enter a valid 10-digit phone number')
.required('Phone number is required'),
password: yup
.string()
.required('Password is required')
.min(8, 'Password must be at least 8 characters')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'
),
confirmPassword: yup
.string()
.required('Please confirm your password')
.oneOf([yup.ref('password')], 'Passwords must match')
});
export default function SignupScreen() {
// Initialize form with React Hook Form
const { control, handleSubmit, formState: { errors } } = useForm<SignupFormData>({
defaultValues: {
fullName: '',
email: '',
phone: '',
password: '',
confirmPassword: ''
},
resolver: yupResolver(validationSchema) // Apply Yup validation
});
// Toggle password visibility state
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
// Form submission handler
const onSubmit = (data: SignupFormData) => {
console.log('Form submitted:', data);
// Implement your signup logic here
// e.g., API call to create user account
};
return (
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.title}>Create Account</Text>
{/* Full Name Input */}
<RhfInput
control={control} // Pass the form control
name="fullName" // Field name must match the form data type
placeholder="Full Name"
autoCapitalize="words"
// Left accessory - icon
left={<Ionicons name="person-outline" size={20} color="gray" />}
/>
{/* Email Input */}
<RhfInput
control={control}
name="email"
placeholder="Email Address"
keyboardType="email-address"
autoCapitalize="none"
left={<Ionicons name="mail-outline" size={20} color="gray" />}
/>
{/* Phone Input */}
<RhfInput
control={control}
name="phone"
placeholder="Phone Number"
keyboardType="phone-pad"
left={<Ionicons name="call-outline" size={20} color="gray" />}
/>
{/* Password Input */}
<RhfInput
control={control}
name="password"
placeholder="Password"
secureTextEntry={!showPassword} // Toggle based on state
left={<Ionicons name="lock-closed-outline" size={20} color="gray" />}
// Right accessory - interactive button to toggle password visibility
right={
<TouchableOpacity onPress={() => setShowPassword(!showPassword)}>
<Ionicons
name={showPassword ? "eye-off-outline" : "eye-outline"}
size={20}
color="gray"
/>
</TouchableOpacity>
}
/>
{/* Confirm Password Input */}
<RhfInput
control={control}
name="confirmPassword"
placeholder="Confirm Password"
secureTextEntry={!showConfirmPassword}
left={<Ionicons name="lock-closed-outline" size={20} color="gray" />}
right={
<TouchableOpacity onPress={() => setShowConfirmPassword(!showConfirmPassword)}>
<Ionicons
name={showConfirmPassword ? "eye-off-outline" : "eye-outline"}
size={20}
color="gray"
/>
</TouchableOpacity>
}
/>
{/* Submit Button */}
<TouchableOpacity
style={styles.button}
onPress={handleSubmit(onSubmit)}
>
<Text style={styles.buttonText}>Sign Up</Text>
</TouchableOpacity>
</ScrollView>
);
}
// Styles for the signup form
const styles = StyleSheet.create({
container: {
padding: 24,
gap: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 24,
textAlign: 'center',
},
button: {
backgroundColor: '#007AFF',
borderRadius: 8,
height: 50,
justifyContent: 'center',
alignItems: 'center',
marginTop: 16,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
});
This example demonstrates a complete signup form implementation that showcases:
Proper TypeScript integration with form data structure
Comprehensive validation with Yup
Multiple field types with appropriate keyboard configurations
Icon accessories on both sides of inputs
Interactive elements (password visibility toggles)
Form submission handling
The comments explain each component of the implementation, making it easy to understand how the RhfInput component integrates with React Hook Form.
Subscribe to my newsletter
Read articles from Arnab Paryali directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Arnab Paryali
Arnab Paryali
Contai, West Bengal, India