React Native Activity Indicator
In modern mobile applications, providing clear feedback during asynchronous operations—such as data fetching, file uploads, or complex computations—is critical for a smooth user experience. Without visual cues, users may perceive the app as unresponsive or broken. A common solution is to display a loading indicator that signals “work in progress.” In React Native, the built-in ActivityIndicator component serves this exact purpose, offering a simple yet versatile spinner for both Android and iOS platforms.
This guide explores everything you need to know about React Native’s ActivityIndicator. We’ll cover:
- Understanding the ActivityIndicator component
- Installing and configuring React Native
- Basic and advanced usage patterns
- Customizing size, color, and position
- Hiding and stopping the spinner
- Platform-specific considerations
- Building custom wrappers and reusable loaders
- Performance and best practices
- Accessibility considerations
- Third-party alternatives and enhancements
By the end of this article, you’ll be equipped with the knowledge to seamlessly integrate loading indicators into your React Native projects, ensuring a polished and user-friendly experience.
1. Getting Started with React Native
If you’re new to React Native, you’ll first need to set up your development environment. The official React Native documentation provides detailed instructions for both Expo CLI and React Native CLI workflows. Below is a quick start using React Native CLI:
# Install React Native CLI globally (if not already) npm install -g react-native-cli # Create a new React Native project react-native init MyLoaderApp # Navigate into the project cd MyLoaderApp # Run on iOS (requires Xcode) npx react-native run-ios # Run on Android (requires Android SDK) npx react-native run-android
Once your app is running on a simulator or device, you can begin integrating the ActivityIndicator.
2. Understanding the ActivityIndicator Component
The ActivityIndicator is a core component provided by React Native:
import React from 'react';
import { View, ActivityIndicator } from 'react-native';
export default function LoadingExample() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator />
</View>
);
}By default, it renders a small spinner ('small') with the platform’s default color (gray on iOS, white on Android when inside a dark container, otherwise gray).
Key Props
- animating
- Type: boolean
- Default: true
- Description: Whether to show the spinner (true) or hide it (false).
- size
- Type: 'small' | 'large' | number
- Default: 'small'
- Description: Size of the spinner. Can be one of the preset strings ('small', 'large') or a custom numeric value (in pixels).
- color
- Type: string
- Default: Platform default
- Description: Color of the spinner; accepts any valid CSS color code.
- hidesWhenStopped
- Type: boolean
- Default: true
- Description: If animating is false, controls whether the spinner is hidden (true) or remains visible (false).
- style
- Type: ViewStyle
- Default: —
- Description: Additional styling for layout and positioning.
3. Basic Usage Examples
Default Spinner
<ActivityIndicator />
Renders a small, gray spinner centered in its parent.
Large Spinner with Custom Color
<ActivityIndicator size="large" color="#3498db" />
Produces a larger spinner colored blue (#3498db).
Numeric Sizes
// 50px spinner
<ActivityIndicator size={50} color="tomato" />When passing a number, the spinner dimensions are set in logical pixels.
4. Positioning and Layout
Typically, you’ll want to center the spinner both vertically and horizontally. Use Flexbox:
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>For absolute positioning (e.g., overlay on top of content):
<View style={{ flex: 1 }}>
{/* Main content */}
<View>{/* Your UI */}</View>
{/* Overlay loader */}
{loading && (
<View style={{
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.5)',
alignItems: 'center',
justifyContent: 'center',
}}>
<ActivityIndicator size="large" color="#fff" />
</View>
)}
</View>StyleSheet.absoluteFillObject expands the loader to cover the entire screen.
5. Controlling Visibility
Often, spinners should appear only during asynchronous operations. Use state:
import React, { useState, useEffect } from 'react';
import { View, ActivityIndicator, Text, Button } from 'react-native';
export default function DataFetcher() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch('https://api.example.com/items');
const json = await response.json();
setData(json);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return (
<View style={{ flex: 1, padding: 20 }}>
{loading ? (
<ActivityIndicator size="large" color="#2ecc71" />
) : data ? (
<Text>{JSON.stringify(data, null, 2)}</Text>
) : (
<Button title="Load Items" onPress={fetchData} />
)}
</View>
);
}Key points:
- Use setLoading(true) before the async operation.
- Wrap fetch calls in try/catch/finally to ensure loading toggles off.
- Conditional rendering switches between loader, data, and a “Load” button.
6. Custom Wrapper Component
For consistency and reusability, create a Loader component:
// Loader.js
import React from 'react';
import { View, ActivityIndicator, StyleSheet } from 'react-native';
export default function Loader({ visible = false, size = 'large', color = '#000' }) {
if (!visible) return null;
return (
<View style={styles.overlay}>
<View style={styles.container}>
<ActivityIndicator size={size} color={color} />
</View>
</View>
);
}
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(255,255,255,0.7)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
container: {
padding: 20,
borderRadius: 10,
backgroundColor: '#fff',
},
});Usage:
import Loader from './Loader';
export default function App() {
const [loading, setLoading] = useState(false);
// ...
return (
<View style={{ flex: 1 }}>
{/* ... */}
<Loader visible={loading} color="tomato" />
</View>
);
}
Advantages
- Centralizes styles and props.
- Easy to adjust overlay color, opacity, padding.
- Improves maintainability.
7. Platform-Specific Spinners
React Native adapts the spinner style to each platform:
- iOS: A thin, smooth spinner that supports color tinting.
- Android: A circular progress bar with a thicker stroke.
If you need platform-specific customization:
import { Platform, ActivityIndicator } from 'react-native';
<ActivityIndicator
size={Platform.OS === 'ios' ? 'large' : 50}
color={Platform.select({ ios: '#007aff', android: '#3ddc84' })}
/>8. Hiding When Stopped
Control visibility without unmounting:
<ActivityIndicator
animating={loading}
size="large"
color="#8e44ad"
hidesWhenStopped={true}
/>- animating={false} stops the spinner.
- With hidesWhenStopped={true}, the spinner is removed from layout.
If you want to maintain layout space even when stopped, set hidesWhenStopped={false}.
9. Accessibility
Ensure that loading indicators are accessible:
<ActivityIndicator size="large" color="#000" accessibilityLabel="Loading content" accessibilityRole="progressbar" />
- accessibilityLabel describes the operation, e.g., “Loading…”
- accessibilityRole="progressbar" signals a progress indicator.
Use React Native Accessibility Props to make your app inclusive.
10. Performance Considerations
While ActivityIndicator is lightweight, consider:
- Avoiding Overuse: Don’t layer multiple spinners; choose a single, clear indicator.
- Minimizing Re-renders: Wrap loaders in React.memo if they receive props from high-frequency state changes.
- Debouncing Short Tasks: If operations complete extremely fast (<200ms), delay showing the spinner to prevent flicker.
Example debounce hook:
function useDebouncedBool(flag, delay = 200) {
const [visible, setVisible] = useState(false);
useEffect(() => {
let timer;
if (flag) {
timer = setTimeout(() => setVisible(true), delay);
} else {
setVisible(false);
}
return () => clearTimeout(timer);
}, [flag, delay]);
return visible;
}
// Usage
const showLoader = useDebouncedBool(loadingFlag);
return <ActivityIndicator animating={showLoader} />;11. Alternatives and Enhancements
While the built-in ActivityIndicator is sufficient for many cases, you may explore:
- Lottie Animations: lottie-react-native for animated, customized loaders.
- Progress Bar Libraries: Horizontal or circular progress bars with percentage display (e.g., react-native-progress).
- Custom SVG Spinners: Use react-native-svg to draw unique spinner designs.
Example: Lottie Integration
npm install lottie-react-native lottie-ios
import LottieView from 'lottie-react-native'; <LottieView source={require('./assets/loader.json')} autoPlay loop style={{ width: 100, height: 100 }} />
12. Real-World Example: Data-Driven List
import React, { useState, useEffect } from 'react';
import { FlatList, View, ActivityIndicator, Text, StyleSheet } from 'react-native';
import Loader from './Loader';
export default function ItemList() {
const [items, setItems] = useState([]);
const [loadingMore, setLoadingMore] = useState(false);
const [page, setPage] = useState(1);
const fetchItems = async (pageNum = 1) => {
setLoadingMore(true);
const response = await fetch(`https://api.example.com/items?page=${pageNum}`);
const json = await response.json();
setItems(prev => [...prev, ...json.data]);
setLoadingMore(false);
};
useEffect(() => {
fetchItems(page);
}, [page]);
return (
<FlatList
data={items}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => <Text style={styles.item}>{item.title}</Text>}
onEndReached={() => setPage(prev => prev + 1)}
ListFooterComponent={() =>
loadingMore ? <ActivityIndicator size="small" /> : null
}
/>
);
}
const styles = StyleSheet.create({
item: {
padding: 16,
borderBottomWidth: 1,
borderColor: '#eee',
},
});This example implements infinite scrolling with a footer spinner that appears as new pages load.
13. Best Practices Checklist
- Debounce spinners for very fast operations to avoid flicker.
- Wrap loaders in reusable components with consistent styling.
- Use accessibility props (accessibilityLabel, accessibilityRole).
- Platform-adapt size and color via Platform.select.
- Consider third-party libraries (Lottie, SVG, progress bars) for richer visuals.
- Optimize performance by minimizing unnecessary renders.
Conclusion
The ActivityIndicator is a powerful yet lightweight tool for signaling in-progress tasks in React Native applications. Whether you need a simple spinner or a fully customized loader, understanding its core props, layout behaviors, and best practices will help you craft seamless user experiences. As your app evolves, consider expanding into animated or SVG-based indicators, but always balance visual appeal with performance and accessibility. Happy coding!