If you are building a React Native app, you are going to need a Login screen, a Registration screen, and probably a "Change Password" screen.
The biggest mistake I see developers make is re-writing the same useState logic to toggle the secureTextEntry (the eye icon) on every single one of these screens. It makes your code messy and violates the DRY (Don't Repeat Yourself) principle.
Instead, we should build a single, beautiful component that handles its own internal state, and just reuse it everywhere.
Here is exactly how to build it using standard React Native components and Expo vector icons.
- The Component Code Create a new file called CustomPasswordInput.tsx in your components folder. We will use @expo/vector-icons for the eye graphic (which comes pre-installed in Expo).
import React, { useState } from 'react';
import { View, TextInput, TouchableOpacity, StyleSheet, TextInputProps } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
// We extend standard TextInputProps so we can pass things like 'placeholder' or 'onChangeText'
interface CustomPasswordInputProps extends TextInputProps {}
export const CustomPasswordInput: React.FC<CustomPasswordInputProps> = ({ ...props }) => {
// Internal state to track if the password is hidden or visible
const [isSecure, setIsSecure] = useState(true);
const toggleVisibility = () => {
setIsSecure(!isSecure);
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
secureTextEntry={isSecure} // Toggles the masking
placeholderTextColor="#999"
{...props}
/>
<TouchableOpacity style={styles.iconContainer} onPress={toggleVisibility}>
<Ionicons
name={isSecure ? "eye-off" : "eye"}
size={24}
color="#666"
/>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F5F5F5',
borderRadius: 8,
borderWidth: 1,
borderColor: '#E0E0E0',
paddingHorizontal: 15,
height: 50,
marginBottom: 15,
},
input: {
flex: 1, // Takes up remaining space
fontSize: 16,
color: '#333',
},
iconContainer: {
padding: 5,
}
});
- Why this approach is better Encapsulation: The Login screen doesn't need to know how the eye icon works. It just needs the final typed text.
Flexibility: Because we spread {...props} onto the TextInput, you can still pass standard props like onChangeText, value, or onBlur from the parent screen without having to manually define them all.
- How clean it looks in your Login Screen Now, look at how incredibly clean your actual Login Screen becomes. No messy state variables tracking the eye icon!
import React, { useState } from 'react';
import { View, Button } from 'react-native';
import { CustomPasswordInput } from '../components/CustomPasswordInput';
export default function LoginScreen() {
const [password, setPassword] = useState('');
const handleLogin = () => {
console.log("Logging in with: ", password);
};
return (
<View style={{ padding: 20 }}>
{/* Our clean, reusable component! */}
<CustomPasswordInput
placeholder="Enter your password"
value={password}
onChangeText={setPassword}
/>
<Button title="Login" onPress={handleLogin} />
</View>
);
}
By extracting UI elements into smart components, you can assemble complex screens in seconds instead of hours.
Did you find this helpful? I build full-stack architectures and post weekly about React Native, .NET Core, and writing cleaner code. Make sure to follow my profile so you don't miss the next UI component breakdown!
Top comments (0)