React Native Components – The Building Blocks
React Native provides a set of core components that map directly to native UI elements. Unlike React for web (where you use <div>, <span>, etc.), React Native uses platform-agnostic components that render to native iOS (UIKit) and Android (Material Design) widgets. Understanding these components, their behavior, and optimization techniques is critical for building performant cross-platform apps in 2026.
- Core Components Overview
Every React Native app is built using these fundamental components. They handle layout, user input, scrolling, and content display.
import React from 'react'; import { View, Text, ScrollView, FlatList, SectionList, Image, TextInput, Button, Pressable, TouchableOpacity, ActivityIndicator, Modal, StatusBar, SafeAreaView, KeyboardAvoidingView, Platform } from 'react-native'; // Component reference table: // View - Container component (like div), handles layout (flexbox, padding, margin) // Text - Displays text (all text must be in <Text>) // ScrollView - Generic scrolling container (renders all children at once) // FlatList - Performant list for large datasets (lazy rendering) // SectionList - FlatList with section headers // Image - Display images (local or remote) // TextInput - Text input field // Button - Simple native button // Pressable - Modern touchable wrapper (recommended over TouchableOpacity) // TouchableOpacity - Legacy touchable with opacity feedback // ActivityIndicator - Loading spinner // Modal - Modal dialog component // StatusBar - Control device status bar // SafeAreaView - Handles notches, status bars, home indicators // KeyboardAvoidingView - Moves content up when keyboard appears
- Layout Components (View, SafeAreaView, KeyboardAvoidingView)
View is the most fundamental component — a container that supports layout with flexbox, style, touch handling, and accessibility. Always wrap content in SafeAreaView on iOS to avoid notches and status bars. Use KeyboardAvoidingView for forms.
import React from 'react'; import { View, Text, SafeAreaView, KeyboardAvoidingView, Platform, StyleSheet } from 'react-native'; const LayoutExample = () => { return ( <SafeAreaView style={styles.safeArea}> {/* KeyboardAvoidingView handles keyboard push-up on iOS/Android */} <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.keyboardView} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20} > {/* Main container using flexbox */} <View style={styles.container}> {/* Header */} <View style={styles.header}> <Text style={styles.title}>Layout Example</Text> </View> {/* Content with flex: 1 takes remaining space */} <View style={styles.content}> <View style={styles.card}> <Text>Card content here</Text> </View> </View> {/* Footer with absolute positioning alternative */} <View style={styles.footer}> <Text>Footer</Text> </View> </View> </KeyboardAvoidingView> </SafeAreaView> ); }; const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: '#f5f5f5' }, keyboardView: { flex: 1 }, container: { flex: 1, padding: 16 }, header: { paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: '#e0e0e0' }, title: { fontSize: 24, fontWeight: 'bold' }, content: { flex: 1, justifyContent: 'center', alignItems: 'center' }, card: { backgroundColor: 'white', padding: 20, borderRadius: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3 // Android shadow }, footer: { paddingVertical: 16, alignItems: 'center' } }); export default LayoutExample; // Flexbox properties in React Native (subset of CSS Flexbox): // flexDirection: 'row' | 'column' (default 'column') // justifyContent: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly' // alignItems: 'flex-start' | 'center' | 'flex-end' | 'stretch' // flexWrap: 'wrap' | 'nowrap' // flex: number (0 = no grow, 1+ = distribute space)
- Text Components & Styling
All text in React Native must be wrapped in a <Text> component. Text components can be nested for styling inheritance. Use numberOfLines for truncation and selectable for user copy.
import React from 'react'; import { View, Text, StyleSheet, Platform } from 'react-native'; const TextComponents = () => { return ( <View style={styles.container}> {/* Basic text */} <Text style={styles.basicText}>Basic Text</Text> {/* Nested text for mixed styles */} <Text style={styles.paragraph}> This is a sentence with{' '} <Text style={styles.boldText}>bold text</Text> {' '} and{' '} <Text style={styles.coloredText}>colored text</Text> {' '} inside. </Text> {/* Truncated text */} <Text numberOfLines={2} ellipsizeMode="tail" style={styles.truncatedText} > This is a very long text that will be truncated after two lines. The user will see an ellipsis at the end. This is useful for previews and cards where space is limited. </Text> {/* Selectable text (users can copy) */} <Text selectable style={styles.selectableText}> This text can be selected and copied by users (iOS menu, Android long-press). </Text> {/* Text with adjustsFontSizeToFit (iOS only) */} {Platform.OS === 'ios' && ( <Text adjustsFontSizeToFit numberOfLines={1} style={styles.adjustingText} > This text shrinks to fit in one line on iOS </Text> )} {/* Text with line height */} <Text style={styles.lineHeightText}> This text has custom line height for better readability. It adds spacing between lines which is important for accessibility. </Text> </View> ); }; const styles = StyleSheet.create({ container: { padding: 16, gap: 16 }, basicText: { fontSize: 16, color: '#333333' }, paragraph: { fontSize: 14, lineHeight: 20, color: '#666666' }, boldText: { fontWeight: 'bold', color: '#000000' }, coloredText: { color: '#007AFF', textDecorationLine: 'underline' }, truncatedText: { fontSize: 14, color: '#666666' }, selectableText: { fontSize: 14, color: '#007AFF', backgroundColor: '#f0f0f0', padding: 8, borderRadius: 4 }, adjustingText: { fontSize: 24, fontWeight: 'bold', width: 200, backgroundColor: '#f0f0f0' }, lineHeightText: { fontSize: 14, lineHeight: 24, color: '#333333' } }); export default TextComponents;
- Scrolling Components: ScrollView vs FlatList
Choose the right scrolling component based on your data size. ScrollView renders all children at once (good for small, fixed content). FlatList renders items lazily (essential for large or dynamic lists).
import React, { useState, useCallback } from 'react'; import { View, Text, ScrollView, FlatList, SectionList, ActivityIndicator, RefreshControl, StyleSheet, SafeAreaView } from 'react-native'; // MARK: - ScrollView (for small/fixed content) const ScrollViewExample = () => { return ( <ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent} // Horizontal scrolling // horizontal={true} // pagingEnabled={true} > {Array.from({ length: 20 }).map((_, index) => ( <View key={index} style={styles.scrollItem}> <Text>Item {index + 1}</Text> </View> ))} </ScrollView> ); }; // MARK: - FlatList (for large, dynamic lists) const FlatListExample = () => { const [data, setData] = useState( Array.from({ length: 100 }).map((_, i) => ({ id: i.toString(), title: `Item ${i + 1}` })) ); const [refreshing, setRefreshing] = useState(false); const [loadingMore, setLoadingMore] = useState(false); // Pull to refresh const onRefresh = useCallback(() => { setRefreshing(true); setTimeout(() => { setData(Array.from({ length: 100 }).map((_, i) => ({ id: i.toString(), title: `Refreshed ${i + 1}` }))); setRefreshing(false); }, 1500); }, []); // Infinite scroll (pagination) const loadMore = useCallback(() => { if (loadingMore) return; setLoadingMore(true); setTimeout(() => { const newItems = Array.from({ length: 20 }).map((_, i) => ({ id: (data.length + i).toString(), title: `Item ${data.length + i + 1}` })); setData(prev => [...prev, ...newItems]); setLoadingMore(false); }, 1000); }, [data.length, loadingMore]); const renderItem = useCallback(({ item, index }) => ( <View style={styles.listItem}> <Text style={styles.itemIndex}>{index + 1}</Text> <Text style={styles.itemTitle}>{item.title}</Text> </View> ), []); const renderFooter = () => { if (!loadingMore) return null; return ( <View style={styles.footerLoader}> <ActivityIndicator size="small" /> <Text style={styles.footerText}>Loading more...</Text> </View> ); }; return ( <SafeAreaView style={styles.safeArea}> <FlatList data={data} renderItem={renderItem} keyExtractor={item => item.id} // Performance optimizations initialNumToRender={10} maxToRenderPerBatch={10} windowSize={21} removeClippedSubviews={true} // Pull to refresh refreshControl={ <RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> } // Infinite scroll onEndReached={loadMore} onEndReachedThreshold={0.5} ListFooterComponent={renderFooter} // Empty state ListEmptyComponent={ <View style={styles.emptyState}> <Text>No items found</Text> </View> } // Optimize for item updates extraData={data.length} /> </SafeAreaView> ); }; // MARK: - SectionList (grouped data with headers) const SectionListExample = () => { const sections = [ { title: 'Fruits', data: ['Apple', 'Banana', 'Orange', 'Mango'] }, { title: 'Vegetables', data: ['Carrot', 'Broccoli', 'Spinach', 'Potato'] }, { title: 'Dairy', data: ['Milk', 'Cheese', 'Yogurt'] } ]; const renderSectionHeader = ({ section: { title } }) => ( <View style={styles.sectionHeader}> <Text style={styles.sectionHeaderText}>{title}</Text> </View> ); const renderItem = ({ item }) => ( <View style={styles.sectionItem}> <Text>{item}</Text> </View> ); return ( <SectionList sections={sections} keyExtractor={(item, index) => item + index} renderSectionHeader={renderSectionHeader} renderItem={renderItem} stickySectionHeadersEnabled={true} contentContainerStyle={styles.sectionList} /> ); }; const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: '#fff' }, scrollContent: { padding: 16, gap: 12 }, scrollItem: { padding: 16, backgroundColor: '#f0f0f0', borderRadius: 8, marginBottom: 8 }, listItem: { flexDirection: 'row', padding: 16, borderBottomWidth: 1, borderBottomColor: '#e0e0e0', alignItems: 'center' }, itemIndex: { width: 40, fontSize: 14, color: '#666' }, itemTitle: { flex: 1, fontSize: 16 }, footerLoader: { padding: 16, alignItems: 'center', flexDirection: 'row', justifyContent: 'center', gap: 8 }, footerText: { fontSize: 12, color: '#666' }, emptyState: { padding: 48, alignItems: 'center' }, sectionHeader: { backgroundColor: '#f8f8f8', padding: 12, borderBottomWidth: 1, borderBottomColor: '#e0e0e0' }, sectionHeaderText: { fontSize: 18, fontWeight: 'bold' }, sectionItem: { padding: 12, borderBottomWidth: 1, borderBottomColor: '#f0f0f0' }, sectionList: { paddingBottom: 20 } }); export { ScrollViewExample, FlatListExample, SectionListExample };
- Touchable Components (Pressable, TouchableOpacity, Buttons)
React Native offers multiple touchable components. Pressable is the modern, recommended approach (React Native 0.63+). It provides fine-grained feedback control and better accessibility than legacy Touchable components.
import React, { useState } from 'react'; import { View, Text, Pressable, TouchableOpacity, TouchableHighlight, Button, Alert, StyleSheet, Vibration, Platform } from 'react-native'; const TouchableComponents = () => { const [pressState, setPressState] = useState(''); // MARK: - Pressable (modern, recommended) const PressableExamples = () => ( <View style={styles.section}> <Text style={styles.sectionTitle}>Pressable (Recommended)</Text> {/* Basic Pressable */} <Pressable onPress={() => Alert.alert('Pressed', 'Basic press detected')} style={styles.pressableBasic} > <Text style={styles.buttonText}>Basic Pressable</Text> </Pressable> {/* Pressable with hover/pressed states */} <Pressable onPress={() => setPressState('Pressed!')} onPressIn={() => setPressState('Pressing...')} onPressOut={() => setPressState('Released')} onLongPress={() => { Vibration.vibrate(100); Alert.alert('Long Press', 'Long press detected'); }} style={({ pressed, hovered }) => [ styles.pressableState, pressed && styles.pressablePressed, hovered && styles.pressableHovered ]} > {({ pressed }) => ( <Text style={styles.buttonText}> {pressed ? 'Holding...' : 'Press with State'} </Text> )} </Pressable> <Text style={styles.pressState}>{pressState}</Text> {/* Pressable with ripple effect (Android) / opacity (iOS) */} <Pressable onPress={() => {}} android_ripple={{ color: '#e0e0e0', borderless: false, radius: 100 }} style={({ pressed }) => [ styles.pressableRipple, Platform.OS === 'ios' && pressed && { opacity: 0.7 } ]} > <Text style={styles.buttonText}>Press with Ripple (Android) / Opacity (iOS)</Text> </Pressable> {/* Disabled state */} <Pressable onPress={() => {}} disabled={true} style={[styles.pressableBasic, styles.disabled]} > <Text style={[styles.buttonText, styles.disabledText]}>Disabled Pressable</Text> </Pressable> </View> ); // MARK: - TouchableOpacity (legacy, still common) const TouchableOpacityExample = () => ( <View style={styles.section}> <Text style={styles.sectionTitle}>TouchableOpacity (Legacy)</Text> <TouchableOpacity activeOpacity={0.6} onPress={() => Alert.alert('TouchableOpacity', 'Simple press')} style={styles.touchableOpacity} > <Text style={styles.buttonText}>TouchableOpacity (fades to 0.6)</Text> </TouchableOpacity> </View> ); // MARK: - TouchableHighlight (for background highlight) const TouchableHighlightExample = () => ( <View style={styles.section}> <Text style={styles.sectionTitle}>TouchableHighlight</Text> <TouchableHighlight underlayColor="#dddddd" onPress={() => Alert.alert('Highlight', 'Background changes color')} style={styles.touchableHighlight} > <Text style={styles.buttonText}>Highlight (background changes)</Text> </TouchableHighlight> </View> ); // MARK: - Button (native, limited styling) const ButtonExample = () => ( <View style={styles.section}> <Text style={styles.sectionTitle}>Native Button</Text> <Button title="Native Button" onPress={() => Alert.alert('Button', 'Native iOS/Android button')} color="#007AFF" disabled={false} /> <Text style={styles.noteText}> Note: Button has limited styling. Use Pressable for custom designs. </Text> </View> ); return ( <ScrollView style={styles.container}> <PressableExamples /> <TouchableOpacityExample /> <TouchableHighlightExample /> <ButtonExample /> </ScrollView> ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, section: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#e0e0e0' }, sectionTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 12 }, pressableBasic: { backgroundColor: '#007AFF', padding: 12, borderRadius: 8, alignItems: 'center', marginBottom: 12 }, pressableState: { backgroundColor: '#34C759', padding: 12, borderRadius: 8, alignItems: 'center', marginBottom: 8 }, pressablePressed: { backgroundColor: '#2BA04E', transform: [{ scale: 0.98 }] }, pressableHovered: { backgroundColor: '#3DD96A' }, pressableRipple: { backgroundColor: '#FF3B30', padding: 12, borderRadius: 8, alignItems: 'center', marginBottom: 12 }, pressState: { fontSize: 12, color: '#666', textAlign: 'center', marginBottom: 12 }, touchableOpacity: { backgroundColor: '#5856D6', padding: 12, borderRadius: 8, alignItems: 'center' }, touchableHighlight: { backgroundColor: '#FF9500', padding: 12, borderRadius: 8 }, buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' }, disabled: { backgroundColor: '#c7c7c7' }, disabledText: { color: '#8e8e8e' }, noteText: { fontSize: 12, color: '#666', marginTop: 8, fontStyle: 'italic' } }); export default TouchableComponents;
- Image Component
The Image component handles both local assets and remote images. Use ImageBackground for placing content on top of images. For performance, specify dimensions and use caching strategies.
import React, { useState } from 'react'; import { View, Text, Image, ImageBackground, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions } from 'react-native'; const { width: screenWidth } = Dimensions.get('window'); const ImageComponents = () => { const [imageLoading, setImageLoading] = useState(true); const [imageError, setImageError] = useState(false); // Local image (requires bundler configuration) const LocalImage = () => ( <Image source={require('../assets/logo.png')} // Local asset style={styles.localImage} resizeMode="contain" // cover, contain, stretch, repeat, center /> ); // Remote image with loading/error states const RemoteImage = () => ( <View style={styles.remoteContainer}> {imageLoading && ( <View style={styles.loaderOverlay}> <ActivityIndicator size="large" color="#007AFF" /> </View> )} {imageError && ( <View style={styles.errorOverlay}> <Text style={styles.errorText}>Failed to load image</Text> </View> )} <Image source={{ uri: 'https://picsum.photos/400/300' }} style={styles.remoteImage} onLoadStart={() => { setImageLoading(true); setImageError(false); }} onLoadEnd={() => setImageLoading(false)} onError={() => { setImageLoading(false); setImageError(true); }} // Caching strategy // iOS: NSURLCache, Android: Disk cache // Cache control: 'default', 'reload', 'force-cache', 'only-if-cached' cache="force-cache" /> </View> ); // ImageBackground (for overlaying text/buttons) const ImageBackgroundExample = () => ( <ImageBackground source={{ uri: 'https://picsum.photos/400/200' }} style={styles.backgroundImage} imageStyle={{ borderRadius: 12 }} resizeMode="cover" > <View style={styles.overlay}> <Text style={styles.overlayText}>Text Overlay on Image</Text> <TouchableOpacity style={styles.overlayButton}> <Text style={styles.overlayButtonText}>Action</Text> </TouchableOpacity> </View> </ImageBackground> ); // Network image with authentication headers const AuthenticatedImage = () => { const [authToken] = useState('your-jwt-token'); return ( <Image source={{ uri: 'https://api.example.com/images/1', headers: { Authorization: `Bearer ${authToken}` } }} style={styles.remoteImage} /> ); }; // Multiple sources for different screen densities const MultipleSourcesImage = () => ( <Image source={{ uri: 'https://example.com/image.jpg', width: 400, height: 300, scale: 2 // For pixel density }} style={styles.remoteImage} /> ); return ( <ScrollView style={styles.container}> <Text style={styles.title}>Image Component Examples</Text> <Text style={styles.subtitle}>Local Image:</Text> <LocalImage /> <Text style={styles.subtitle}>Remote Image with Loading:</Text> <RemoteImage /> <Text style={styles.subtitle}>ImageBackground:</Text> <ImageBackgroundExample /> <Text style={styles.note}> Performance tips:• Always set width/height for remote images • Use resizeMode appropriately • Consider using react-native-fast-image for advanced caching • Use thumbnail URIs for previews </Text> </ScrollView> ); }; const styles = StyleSheet.create({ container: { flex: 1, padding: 16, backgroundColor: '#fff' }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 20 }, subtitle: { fontSize: 18, fontWeight: '600', marginTop: 16, marginBottom: 8 }, localImage: { width: 150, height: 150, alignSelf: 'center', marginVertical: 10 }, remoteContainer: { position: 'relative', alignItems: 'center', justifyContent: 'center' }, remoteImage: { width: screenWidth - 32, height: 200, borderRadius: 8, marginVertical: 10 }, loaderOverlay: { position: 'absolute', width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.1)', borderRadius: 8, zIndex: 1 }, errorOverlay: { position: 'absolute', width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 8, zIndex: 1 }, errorText: { color: '#fff', fontSize: 14 }, backgroundImage: { width: screenWidth - 32, height: 200, borderRadius: 12, overflow: 'hidden', marginVertical: 10 }, overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'center', alignItems: 'center', padding: 20 }, overlayText: { color: '#fff', fontSize: 20, fontWeight: 'bold', textAlign: 'center', marginBottom: 12 }, overlayButton: { backgroundColor: '#007AFF', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8 }, overlayButtonText: { color: '#fff', fontWeight: '600' }, note: { fontSize: 12, color: '#666', marginTop: 20, padding: 12, backgroundColor: '#f5f5f5', borderRadius: 8 } }); export default ImageComponents;
- Custom Component Creation & Composition
Creating reusable custom components is essential for maintaining large React Native apps. Use props for customization, children for composition, and TypeScript for type safety.
import React from 'react'; import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, TextStyle, ActivityIndicator } from 'react-native'; // MARK: - Typed Props Interface (TypeScript) interface ButtonProps { title: string; onPress: () => void; variant?: 'primary' | 'secondary' | 'danger'; size?: 'small' | 'medium' | 'large'; loading?: boolean; disabled?: boolean; style?: ViewStyle; textStyle?: TextStyle; leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; } // MARK: - Custom Button Component const CustomButton: React.FC<ButtonProps> = ({ title, onPress, variant = 'primary', size = 'medium', loading = false, disabled = false, style, textStyle, leftIcon, rightIcon }) => { const getButtonStyles = (): ViewStyle[] => { const baseStyles = [styles.button]; // Variant styles if (variant === 'primary') baseStyles.push(styles.primary); if (variant === 'secondary') baseStyles.push(styles.secondary); if (variant === 'danger') baseStyles.push(styles.danger); // Size styles if (size === 'small') baseStyles.push(styles.small); if (size === 'medium') baseStyles.push(styles.medium); if (size === 'large') baseStyles.push(styles.large); // Disabled style if (disabled) baseStyles.push(styles.disabled); if (style) baseStyles.push(style); return baseStyles; }; const getTextStyles = (): TextStyle[] => { const baseStyles = [styles.text]; if (size === 'small') baseStyles.push(styles.textSmall); if (size === 'large') baseStyles.push(styles.textLarge); if (disabled) baseStyles.push(styles.textDisabled); if (textStyle) baseStyles.push(textStyle); return baseStyles; }; return ( <TouchableOpacity onPress={onPress} disabled={disabled || loading} activeOpacity={0.7} style={getButtonStyles()} > {loading ? ( <ActivityIndicator color="#fff" size="small" /> ) : ( <View style={styles.buttonContent}> {leftIcon && <View style={styles.leftIcon}>{leftIcon}</View>} <Text style={getTextStyles()}>{title}</Text> {rightIcon && <View style={styles.rightIcon}>{rightIcon}</View>} </View> )} </TouchableOpacity> ); }; // MARK: - Card Component with Children Composition interface CardProps { title?: string; subtitle?: string; footer?: React.ReactNode; children: React.ReactNode; style?: ViewStyle; onPress?: () => void; } const Card: React.FC<CardProps> = ({ title, subtitle, footer, children, style, onPress }) => { const Content = () => ( <View style={[styles.card, style]}> {(title || subtitle) && ( <View style={styles.cardHeader}> {title && <Text style={styles.cardTitle}>{title}</Text>} {subtitle && <Text style={styles.cardSubtitle}>{subtitle}</Text>} </View> )} <View style={styles.cardContent}> {children} </View> {footer && ( <View style={styles.cardFooter}> {footer} </View> )} </View> ); if (onPress) { return ( <TouchableOpacity onPress={onPress} activeOpacity={0.8}> <Content /> </TouchableOpacity> ); } return <Content />; }; // MARK: - List Item Component (optimized for FlatList) interface ListItemProps { id: string; title: string; description?: string; avatar?: React.ReactNode; onPress?: (id: string) => void; rightElement?: React.ReactNode; } const ListItem = React.memo(({ id, title, description, avatar, onPress, rightElement }: ListItemProps) => { const handlePress = () => { onPress?.(id); }; return ( <TouchableOpacity onPress={handlePress} style={styles.listItem}> {avatar && <View style={styles.listItemAvatar}>{avatar}</View>} <View style={styles.listItemContent}> <Text style={styles.listItemTitle}>{title}</Text> {description && ( <Text style={styles.listItemDescription} numberOfLines={2}> {description} </Text> )} </View> {rightElement && ( <View style={styles.listItemRight}>{rightElement}</View> )} </TouchableOpacity> ); }); // MARK: - Usage Example const CustomComponentsExample = () => { return ( <ScrollView style={styles.container} contentContainerStyle={styles.content}> <CustomButton title="Primary Button" onPress={() => console.log('Pressed')} variant="primary" size="large" /> <CustomButton title="Loading" onPress={() => {}} loading={true} /> <CustomButton title="Disabled" onPress={() => {}} disabled={true} /> <Card title="Card Title" subtitle="Card subtitle goes here" footer={<CustomButton title="Action" onPress={() => {}} size="small" />} onPress={() => console.log('Card pressed')} > <Text>This is the card content. Children can be any React Native component.</Text> </Card> <ListItem id="1" title="List Item Title" description="This is a longer description that will be truncated after two lines if it exceeds the maximum allowed space." onPress={(id) => console.log(`Pressed item ${id}`)} rightElement={<CustomButton title="Action" onPress={() => {}} size="small" />} /> </ScrollView> ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f5f5f5' }, content: { padding: 16, gap: 12 }, // Button styles button: { borderRadius: 8, alignItems: 'center', justifyContent: 'center' }, buttonContent: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }, primary: { backgroundColor: '#007AFF' }, secondary: { backgroundColor: '#5856D6' }, danger: { backgroundColor: '#FF3B30' }, small: { paddingHorizontal: 12, paddingVertical: 6 }, medium: { paddingHorizontal: 16, paddingVertical: 10 }, large: { paddingHorizontal: 24, paddingVertical: 14 }, disabled: { backgroundColor: '#c7c7c7' }, text: { color: '#fff', fontWeight: '600' }, textSmall: { fontSize: 12 }, textLarge: { fontSize: 18 }, textDisabled: { color: '#8e8e8e' }, leftIcon: { marginRight: 8 }, rightIcon: { marginLeft: 8 }, // Card styles card: { backgroundColor: '#fff', borderRadius: 12, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3 }, cardHeader: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#e0e0e0' }, cardTitle: { fontSize: 18, fontWeight: 'bold' }, cardSubtitle: { fontSize: 14, color: '#666', marginTop: 4 }, cardContent: { padding: 16 }, cardFooter: { padding: 12, borderTopWidth: 1, borderTopColor: '#e0e0e0', alignItems: 'flex-end' }, // List item styles listItem: { flexDirection: 'row', backgroundColor: '#fff', padding: 12, borderRadius: 8, alignItems: 'center' }, listItemAvatar: { marginRight: 12 }, listItemContent: { flex: 1 }, listItemTitle: { fontSize: 16, fontWeight: '600' }, listItemDescription: { fontSize: 14, color: '#666', marginTop: 4 }, listItemRight: { marginLeft: 12 } }); export { CustomButton, Card, ListItem, CustomComponentsExample };
- Performance Optimization for Components
Key optimization techniques for React Native components:
- useMemo & useCallback – Memoize expensive calculations and functions
- React.memo – Prevent unnecessary re-renders of pure components
- FlatList optimizations –
initialNumToRender,maxToRenderPerBatch,windowSize - Avoid inline functions in render (creates new references)
- Use InteractionManager for post-animation tasks
- Image optimization – Proper dimensions, caching, lazy loading
- Avoid anonymous objects in style props (creates new style objects on each render)
import React, { useState, useCallback, useMemo, memo } from 'react'; import { View, Text, FlatList, TouchableOpacity, StyleSheet, InteractionManager } from 'react-native'; // MARK: - Optimized Component with React.memo const OptimizedListItem = memo(({ item, onPress, isSelected }) => { // Log to check re-renders (remove in production) console.log(`Rendering item ${item.id}`); return ( <TouchableOpacity onPress={() => onPress(item.id)} style={[styles.item, isSelected && styles.selectedItem]} > <Text style={styles.itemTitle}>{item.title}</Text> <Text style={styles.itemSubtitle}>{item.subtitle}</Text> </TouchableOpacity> ); }); // MARK: - Parent Component with Optimizations const OptimizedList = () => { const [items, setItems] = useState( Array.from({ length: 1000 }).map((_, i) => ({ id: i, title: `Item ${i}`, subtitle: `Subtitle for item ${i}` })) ); const [selectedId, setSelectedId] = useState(null); const [isReady, setIsReady] = useState(false); // useCallback prevents function recreation on each render const handlePress = useCallback((id) => { setSelectedId(id); console.log(`Selected item ${id}`); }, []); // useMemo for expensive computations const expensiveComputation = useMemo(() => { console.log('Computing expensive value...'); return items.length * 2; }, [items.length]); // Defer non-critical work after animations React.useEffect(() => { InteractionManager.runAfterInteractions(() => { // Load analytics, pre-fetch images, etc. setIsReady(true); }); }, []); // Optimized renderItem function (defined outside FlatList or useCallback) const renderItem = useCallback(({ item }) => ( <OptimizedListItem item={item} onPress={handlePress} isSelected={selectedId === item.id} /> ), [selectedId, handlePress]); // Key extractor (stable, unique) const keyExtractor = useCallback((item) => item.id.toString(), []); // Get item layout for performance const getItemLayout = useCallback((data, index) => ({ length: 70, // Item height offset: 70 * index, index }), []); if (!isReady) { return ( <View style={styles.loadingContainer}> <Text>Loading...</Text> </View> ); } return ( <View style={styles.container}> <Text style={styles.header}>Optimized FlatList</Text> <Text>Expensive value: {expensiveComputation}</Text> <FlatList data={items} renderItem={renderItem} keyExtractor={keyExtractor} getItemLayout={getItemLayout} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={21} removeClippedSubviews={true} updateCellsBatchingPeriod={50} // Disable virtualization for small lists // virtualized={items.length > 100} /> </View> ); }; // MARK: - Avoid Inline Styles // BAD: Creates new style object on each render // <View style={{ padding: 10, margin: 5 }} /> // GOOD: Define styles outside component const styles = StyleSheet.create({ container: { flex: 1, padding: 16 }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' }, header: { fontSize: 24, fontWeight: 'bold', marginBottom: 16 }, item: { padding: 12, borderBottomWidth: 1, borderBottomColor: '#e0e0e0' }, selectedItem: { backgroundColor: '#e3f2fd' }, itemTitle: { fontSize: 16, fontWeight: '600' }, itemSubtitle: { fontSize: 12, color: '#666', marginTop: 4 } }); export default OptimizedList;
- Platform-Specific Components
Use Platform.select() and platform-specific file extensions (.ios.js / .android.js) to customize components per platform.
import React from 'react'; import { View, Text, StyleSheet, Platform, ActionSheetIOS, Alert, ToastAndroid, Switch, ScrollView } from 'react-native'; const PlatformSpecificComponents = () => { // Platform-specific styles const styles = StyleSheet.create({ container: { flex: 1, padding: 16, // Platform-specific padding paddingTop: Platform.OS === 'ios' ? 40 : 20, // Platform-specific shadow ...Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1 }, android: { elevation: 4 } }) }, button: { ...Platform.select({ ios: { backgroundColor: '#007AFF', borderRadius: 8 }, android: { backgroundColor: '#6200EE', borderRadius: 4, elevation: 2 } }), padding: 12, alignItems: 'center', marginVertical: 8 }, buttonText: { color: '#fff', fontWeight: '600' } }); // Platform-specific alerts const showAlert = () => { if (Platform.OS === 'ios') { ActionSheetIOS.showActionSheetWithOptions( { options: ['Cancel', 'Option 1', 'Option 2'], cancelButtonIndex: 0, destructiveButtonIndex: 2, title: 'Choose an option', message: 'Select from the list below' }, (buttonIndex) => { console.log('Selected index:', buttonIndex); } ); } else { Alert.alert( 'Choose an option', 'Select from the list below', [ { text: 'Cancel', style: 'cancel' }, { text: 'Option 1', onPress: () => console.log('Option 1') }, { text: 'Option 2', style: 'destructive', onPress: () => console.log('Option 2') } ] ); } }; // Platform-specific toast const showToast = () => { if (Platform.OS === 'android') { ToastAndroid.show('This is a toast message', ToastAndroid.SHORT); } else { Alert.alert('Notification', 'iOS uses alerts instead of toasts'); } }; // Platform-specific component rendering const renderSwitch = () => { if (Platform.OS === 'ios') { return ( <View style={styles.button}> <Text style={styles.buttonText}>iOS Native Switch Below</Text> <Switch value={true} onValueChange={() => {}} /> </View> ); } else { return ( <View style={styles.button}> <Text style={styles.buttonText}>Android Switch</Text> <Switch value={true} onValueChange={() => {}} /> </View> ); } }; return ( <ScrollView style={styles.container}> <Text style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 16 }}> Platform: {Platform.OS} </Text> <Text style={{ marginBottom: 16 }}> Version: {Platform.Version} </Text> <TouchableOpacity style={styles.button} onPress={showAlert}> <Text style={styles.buttonText}>Show Alert/ActionSheet</Text> </TouchableOpacity> <TouchableOpacity style={styles.button} onPress={showToast}> <Text style={styles.buttonText}>Show Toast/Notification</Text> </TouchableOpacity> {renderSwitch()} <Text style={{ marginTop: 20, fontSize: 12, color: '#666' }}> Tip: Use .ios.js and .android.js file extensions for complete platform separation </Text> </ScrollView> ); }; // Platform-specific file structure: // Button.ios.js – iOS implementation // Button.android.js – Android implementation // Then import: import Button from './Button'; export default PlatformSpecificComponents;