Loading States
Implement effective loading states and user feedback for async operations in React Native applications. This is a foundational concept in cross-platform mobile development that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world React Native experience. Take your time with each section and practice the examples
Loading State Patterns
Provide visual feedback during async operations to improve user experience. Use loading indicators, skeleton screens, and progress bars to show that data is being fetched.. This is an essential concept that every React Native developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
Loading Indicators
- ActivityIndicator - Built-in loading spinner — a critical concept in cross-platform mobile development that you will use frequently in real projects
- Custom loading components - Branded loading animations
- Skeleton screens - Placeholder content while loading
- Progress bars - For long-running operations — a critical concept in cross-platform mobile development that you will use frequently in real projects
- Pull-to-refresh - For refreshing data — a critical concept in cross-platform mobile development that you will use frequently in real projects
Loading State Example
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
FlatList,
RefreshControl
} from 'react-native';
// Mock API with delay
const mockApi = {
fetchUsers: async () => {
await new Promise(resolve => setTimeout(resolve, 2000));
return [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com' },
];
},
};
// Skeleton component
const SkeletonItem = () => (
<View style={styles.skeletonItem}>
<View style={styles.skeletonAvatar} />
<View style={styles.skeletonContent}>
<View style={styles.skeletonLine} />
<View style={[styles.skeletonLine, styles.skeletonLineShort]} />
</View>
</View>
);
// Loading component
const LoadingComponent = ({ message = 'Loading...' }) => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#2196F3" />
<Text style={styles.loadingText}>{message}</Text>
</View>
);
// Main component
const LoadingStates = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState(null);
const loadUsers = async (isRefresh = false) => {
if (isRefresh) {
setRefreshing(true);
} else {
setLoading(true);
}
setError(null);
try {
const userData = await mockApi.fetchUsers();
setUsers(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
setRefreshing(false);
}
};
const onRefresh = () => {
loadUsers(true);
};
useEffect(() => {
loadUsers();
}, []);
const renderUser = ({ item }) => (
<View style={styles.userItem}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>
{item.name.charAt(0).toUpperCase()}
</Text>
</View>
<View style={styles.userInfo}>
<Text style={styles.userName}>{item.name}</Text>
<Text style={styles.userEmail}>{item.email}</Text>
</View>
</View>
);
if (loading) {
return (
<View style={styles.container}>
<Text style={styles.title}>Loading States Example</Text>
<LoadingComponent message="Fetching users..." />
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Loading States Example</Text>
<TouchableOpacity style={styles.button} onPress={() => loadUsers()}>
<Text style={styles.buttonText}>Reload Users</Text>
</TouchableOpacity>
{error && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{error}</Text>
</View>
)}
<FlatList
data={users}
keyExtractor={item => item.id.toString()}
renderItem={renderUser}
style={styles.list}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={['#2196F3']}
tintColor="#2196F3"
/>
}
ListEmptyComponent={
!loading && !error ? (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>No users found</Text>
</View>
) : null
}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 30,
color: '#333',
},
button: {
backgroundColor: '#2196F3',
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginBottom: 20,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
fontSize: 16,
color: '#666',
marginTop: 10,
},
skeletonItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 15,
backgroundColor: 'white',
marginBottom: 10,
borderRadius: 8,
},
skeletonAvatar: {
width: 50,
height: 50,
borderRadius: 25,
backgroundColor: '#e0e0e0',
marginRight: 15,
},
skeletonContent: {
flex: 1,
},
skeletonLine: {
height: 16,
backgroundColor: '#e0e0e0',
borderRadius: 4,
marginBottom: 8,
},
skeletonLineShort: {
width: '60%',
},
userItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 15,
backgroundColor: 'white',
marginBottom: 10,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
avatar: {
width: 50,
height: 50,
borderRadius: 25,
backgroundColor: '#2196F3',
justifyContent: 'center',
alignItems: 'center',
marginRight: 15,
},
avatarText: {
color: 'white',
fontSize: 20,
fontWeight: 'bold',
},
userInfo: {
flex: 1,
},
userName: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
marginBottom: 4,
},
userEmail: {
fontSize: 14,
color: '#666',
},
list: {
flex: 1,
},
errorContainer: {
backgroundColor: '#ffebee',
padding: 15,
borderRadius: 8,
marginBottom: 20,
},
errorText: {
color: '#f44336',
fontSize: 14,
textAlign: 'center',
},
emptyContainer: {
alignItems: 'center',
padding: 40,
},
emptyText: {
fontSize: 16,
color: '#999',
fontStyle: 'italic',
},
});
export default LoadingStates;