Building a Reusable React Native Input Component with React Hook Form
Learn how to build a type-safe, reusable React Native TextInput component that integrates seamlessly with React Hook Form and supports custom accessories.
- react-native
- react-hook-form
- typescript
- form-validation
- yup
Forms in React Native apps often suffer from repetitive code, validation complexity, and inconsistent styling. This guide demonstrates how to build a reusable, type-safe input component that integrates with React Hook Form.
Requirements
The component must:
- Integrate with React Hook Form for validation and state management
- Support standard React Native
TextInputprops - Allow custom elements on either side of the input
- Handle error display consistently
- Maintain TypeScript type safety
Component Interface
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.
React Hook Form Integration
The core integration uses React Hook Form’s Controller component:
<Controller
control={control}
name={name}
render={({
field: { onChange, ...fieldProps },
fieldState: { error },
}) => (
// Render the input with validation state
)}
/>
This provides access to the field’s current value, change handlers, and validation state.
The Final Component
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;
Styles are defined outside the component to prevent recreation on each render.
Usage Example
A complete signup form demonstrating the component:
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';
type SignupFormData = {
fullName: string;
email: string;
phone: string;
password: string;
confirmPassword: string;
};
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() {
const { control, handleSubmit } = useForm<SignupFormData>({
defaultValues: {
fullName: '',
email: '',
phone: '',
password: '',
confirmPassword: ''
},
resolver: yupResolver(validationSchema)
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const onSubmit = (data: SignupFormData) => {
console.log('Form submitted:', data);
};
return (
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.title}>Create Account</Text>
<RhfInput
control={control}
name="fullName"
placeholder="Full Name"
autoCapitalize="words"
left={<Ionicons name="person-outline" size={20} color="gray" />}
/>
<RhfInput
control={control}
name="email"
placeholder="Email Address"
keyboardType="email-address"
autoCapitalize="none"
left={<Ionicons name="mail-outline" size={20} color="gray" />}
/>
<RhfInput
control={control}
name="phone"
placeholder="Phone Number"
keyboardType="phone-pad"
left={<Ionicons name="call-outline" size={20} color="gray" />}
/>
<RhfInput
control={control}
name="password"
placeholder="Password"
secureTextEntry={!showPassword}
left={<Ionicons name="lock-closed-outline" size={20} color="gray" />}
right={
<TouchableOpacity onPress={() => setShowPassword(!showPassword)}>
<Ionicons
name={showPassword ? "eye-off-outline" : "eye-outline"}
size={20}
color="gray"
/>
</TouchableOpacity>
}
/>
<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>
}
/>
<TouchableOpacity
style={styles.button}
onPress={handleSubmit(onSubmit)}
>
<Text style={styles.buttonText}>Sign Up</Text>
</TouchableOpacity>
</ScrollView>
);
}
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',
},
});
The example demonstrates proper TypeScript integration, comprehensive Yup validation, multiple field types with appropriate keyboard configurations, icon accessories, interactive password visibility toggles, and complete form submission handling.