Introduction
Understanding component React Native lifecycle methods is crucial for building efficient React Native applications. Component lifecycle refers to the series of methods that are called at different stages of a component’s existence – from creation to destruction. This chapter will explore both class component lifecycle methods and their functional component equivalents using hooks.
Mastering lifecycle methods helps you:
- Optimize performance by controlling when components update
- Manage side effects like API calls and subscriptions
- Clean up resources to prevent memory leaks
- Handle component state changes effectively
Component Lifecycle Overview
Every React Native component goes through three main phases:
- Mounting: Component is being created and inserted into the DOM
- Updating: Component is being re-rendered as a result of changes to props or state
- Unmounting: Component is being removed from the DOM
Class Component Lifecycle Methods
Mounting Phase
constructor()
Called before the component is mounted. Used for initializing state and binding methods.
import React, { Component } from 'react';
import { View, Text, StyleSheet } from 'react-native';
class LifecycleExample extends Component {
constructor(props) {
super(props);
// Initialize state
this.state = {
count: 0,
data: null,
};
// Bind methods (though arrow functions are preferred)
this.handleIncrement = this.handleIncrement.bind(this);
console.log('1. Constructor called');
}
handleIncrement() {
this.setState({ count: this.state.count + 1 });
}
render() {
console.log('2. Render called');
return (
<View style={styles.container}>
<Text>Count: {this.state.count}</Text>
</View>
);
}
}
componentDidMount()
Called immediately after the component is mounted. Perfect for API calls, subscriptions, and DOM manipulation.
class DataFetcher extends Component {
constructor(props) {
super(props);
this.state = {
data: null,
loading: true,
error: null,
};
}
async componentDidMount() {
console.log('3. componentDidMount called');
try {
// API call
const response = await fetch('https://api.example.com/data');
const data = await response.json();
this.setState({
data,
loading: false
});
// Set up subscriptions
this.setupEventListeners();
} catch (error) {
this.setState({
error: error.message,
loading: false
});
}
}
setupEventListeners() {
// Example: Listen to app state changes
this.appStateSubscription = AppState.addEventListener(
'change',
this.handleAppStateChange
);
}
handleAppStateChange = (nextAppState) => {
if (nextAppState === 'active') {
// App came to foreground
this.refreshData();
}
};
render() {
const { data, loading, error } = this.state;
if (loading) {
return (
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color="#0000ff" />
</View>
);
}
if (error) {
return (
<View style={styles.centerContainer}>
<Text style={styles.errorText}>Error: {error}</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text>{JSON.stringify(data, null, 2)}</Text>
</View>
);
}
}
Updating Phase
componentDidUpdate()
Called immediately after updating occurs. Used for DOM operations and additional API calls based on prop/state changes.
class UserProfile extends Component {
constructor(props) {
super(props);
this.state = {
userDetails: null,
previousUserId: null,
};
}
componentDidMount() {
this.fetchUserDetails(this.props.userId);
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate called');
// Check if userId prop changed
if (prevProps.userId !== this.props.userId) {
this.fetchUserDetails(this.props.userId);
}
// Check if user details changed
if (prevState.userDetails !== this.state.userDetails) {
this.logUserActivity();
}
// Example: Scroll to top when data changes
if (prevState.userDetails !== this.state.userDetails && this.scrollViewRef) {
this.scrollViewRef.scrollTo({ x: 0, y: 0, animated: true });
}
}
fetchUserDetails = async (userId) => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const userDetails = await response.json();
this.setState({ userDetails });
} catch (error) {
console.error('Failed to fetch user details:', error);
}
};
logUserActivity = () => {
// Log user activity for analytics
console.log('User details updated');
};
render() {
const { userDetails } = this.state;
return (
<ScrollView
ref={ref => this.scrollViewRef = ref}
style={styles.container}
>
{userDetails && (
<View>
<Text style={styles.title}>{userDetails.name}</Text>
<Text style={styles.subtitle}>{userDetails.email}</Text>
</View>
)}
</ScrollView>
);
}
}
getSnapshotBeforeUpdate()
Called right before the most recently rendered output is committed. Rarely used but useful for capturing scroll position.
class ChatMessages extends Component {
constructor(props) {
super(props);
this.state = {
messages: [],
};
this.messagesEndRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Capture scroll position before update
if (prevState.messages.length < this.state.messages.length) {
const { scrollTop, scrollHeight } = this.messagesEndRef.current;
return { scrollTop, scrollHeight };
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// Auto-scroll to bottom if user was already at bottom
if (snapshot !== null) {
const { scrollTop, scrollHeight } = snapshot;
const isAtBottom = scrollTop + 300 >= scrollHeight;
if (isAtBottom) {
this.scrollToBottom();
}
}
}
scrollToBottom = () => {
this.messagesEndRef.current?.scrollToEnd({ animated: true });
};
render() {
return (
<FlatList
ref={this.messagesEndRef}
data={this.state.messages}
renderItem={({ item }) => <MessageItem message={item} />}
keyExtractor={item => item.id}
/>
);
}
}
Unmounting Phase
componentWillUnmount()
Called immediately before a component is unmounted and destroyed. Clean up subscriptions, timers, and cancel network requests.
class TimerComponent extends Component {
constructor(props) {
super(props);
this.state = {
seconds: 0,
};
this.timerRef = null;
}
componentDidMount() {
// Start timer
this.timerRef = setInterval(() => {
this.setState(prevState => ({
seconds: prevState.seconds + 1,
}));
}, 1000);
// Set up other subscriptions
this.setupSubscriptions();
}
componentWillUnmount() {
console.log('componentWillUnmount called');
// Clean up timer
if (this.timerRef) {
clearInterval(this.timerRef);
}
// Clean up subscriptions
this.cleanupSubscriptions();
// Cancel any pending API requests
if (this.cancelToken) {
this.cancelToken.cancel('Component unmounted');
}
}
setupSubscriptions = () => {
// Example subscriptions
this.keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow',
this.keyboardDidShow
);
this.keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide',
this.keyboardDidHide
);
};
cleanupSubscriptions = () => {
// Remove listeners
this.keyboardDidShowListener?.remove();
this.keyboardDidHideListener?.remove();
};
keyboardDidShow = () => {
console.log('Keyboard shown');
};
keyboardDidHide = () => {
console.log('Keyboard hidden');
};
render() {
return (
<View style={styles.container}>
<Text style={styles.timer}>Timer: {this.state.seconds}s</Text>
</View>
);
}
}
Functional Component Lifecycle with Hooks
useEffect Hook – The Lifecycle Replacement
The useEffect hook combines componentDidMount, componentDidUpdate, and componentWillUnmount into a single API.
Basic useEffect Usage
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
const FunctionalLifecycleExample = () => {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
// Equivalent to componentDidMount and componentDidUpdate
useEffect(() => {
console.log('Component mounted or count updated');
document.title = `Count: ${count}`;
}, [count]); // Dependency array
// Equivalent to componentDidMount only
useEffect(() => {
console.log('Component mounted');
fetchData();
}, []); // Empty dependency array
// Equivalent to componentWillUnmount
useEffect(() => {
const timer = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// Cleanup function (componentWillUnmount)
return () => {
clearInterval(timer);
};
}, []);
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
}
};
return (
<View style={styles.container}>
<Text>Count: {count}</Text>
<Text>Data: {data ? JSON.stringify(data) : 'Loading...'}</Text>
</View>
);
};
Advanced useEffect Patterns
1. Custom Hook for API Calls
import { useState, useEffect } from 'react';
const useApi = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
const result = await response.json();
if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
// Cleanup function
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
};
// Usage
const UserProfile = ({ userId }) => {
const { data: user, loading, error } = useApi(`/api/users/${userId}`);
if (loading) return <Text>Loading...</Text>;
if (error) return <Text>Error: {error}</Text>;
if (!user) return <Text>No user found</Text>;
return (
<View>
<Text>{user.name}</Text>
<Text>{user.email}</Text>
</View>
);
};
2. useEffect with Subscriptions
import React, { useState, useEffect } from 'react';
import { View, Text, AppState } from 'react-native';
const AppStateExample = () => {
const [appState, setAppState] = useState(AppState.currentState);
useEffect(() => {
const handleAppStateChange = (nextAppState) => {
setAppState(nextAppState);
if (nextAppState === 'active') {
// App came to foreground
console.log('App is active');
} else if (nextAppState === 'background') {
// App went to background
console.log('App is in background');
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
// Cleanup subscription
return () => {
subscription?.remove();
};
}, []);
return (
<View>
<Text>Current app state: {appState}</Text>
</View>
);
};
3. useEffect with Cleanup
import React, { useState, useEffect } from 'react';
import { View, Text, Keyboard } from 'react-native';
const KeyboardExample = () => {
const [keyboardVisible, setKeyboardVisible] = useState(false);
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow',
() => {
setKeyboardVisible(true);
}
);
const keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide',
() => {
setKeyboardVisible(false);
}
);
// Cleanup function
return () => {
keyboardDidShowListener.remove();
keyboardDidHideListener.remove();
};
}, []);
return (
<View>
<Text>Keyboard is {keyboardVisible ? 'visible' : 'hidden'}</Text>
</View>
);
};
Advanced Lifecycle Patterns
1. Error Boundaries (Class Components Only)
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Update state to show error UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught by boundary:', error, errorInfo);
// Example: Send to crash reporting service
// crashReporting.recordError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<View style={styles.errorContainer}>
<Text style={styles.errorTitle}>Something went wrong</Text>
<Text style={styles.errorMessage}>
{this.state.error?.message || 'An unexpected error occurred'}
</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={() => this.setState({ hasError: false, error: null })}
>
<Text style={styles.retryButtonText}>Try Again</Text>
</TouchableOpacity>
</View>
);
}
return this.props.children;
}
}
// Usage
const App = () => (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
2. Performance Optimization with React.memo
import React, { memo, useState, useEffect } from 'react';
const ExpensiveComponent = memo(({ data, onUpdate }) => {
console.log('ExpensiveComponent rendered');
// Expensive computation
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: true,
timestamp: Date.now(),
}));
}, [data]);
return (
<View>
{processedData.map(item => (
<Text key={item.id}>{item.name}</Text>
))}
</View>
);
});
// Custom comparison function
const CustomMemoComponent = memo(({ user, posts }) => {
return (
<View>
<Text>{user.name}</Text>
<Text>Posts: {posts.length}</Text>
</View>
);
}, (prevProps, nextProps) => {
// Custom comparison logic
return (
prevProps.user.id === nextProps.user.id &&
prevProps.posts.length === nextProps.posts.length
);
});
3. Custom Hooks for Lifecycle Management
import { useEffect, useRef } from 'react';
// Custom hook for previous value
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
// Custom hook for component did mount
const useComponentDidMount = (callback) => {
useEffect(callback, []);
};
// Custom hook for component will unmount
const useComponentWillUnmount = (callback) => {
useEffect(() => {
return callback;
}, []);
};
// Custom hook for interval
const useInterval = (callback, delay) => {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => {
savedCallback.current();
};
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
// Usage examples
const ExampleComponent = ({ userId }) => {
const [user, setUser] = useState(null);
const [count, setCount] = useState(0);
const previousUserId = usePrevious(userId);
// Mount effect
useComponentDidMount(() => {
console.log('Component mounted');
});
// Unmount effect
useComponentWillUnmount(() => {
console.log('Component will unmount');
});
// Interval hook
useInterval(() => {
setCount(count => count + 1);
}, 1000);
// Effect when userId changes
useEffect(() => {
if (previousUserId !== userId) {
console.log(`User ID changed from ${previousUserId} to ${userId}`);
// Fetch new user data
}
}, [userId, previousUserId]);
return (
<View>
<Text>Count: {count}</Text>
<Text>User ID: {userId}</Text>
</View>
);
};
Lifecycle Best Practices
1. Avoid Common Mistakes
// ❌ Bad: Missing cleanup
const BadComponent = () => {
useEffect(() => {
const timer = setInterval(() => {
console.log('Timer tick');
}, 1000);
// Missing cleanup - memory leak!
}, []);
return <View />;
};
// ✅ Good: Proper cleanup
const GoodComponent = () => {
useEffect(() => {
const timer = setInterval(() => {
console.log('Timer tick');
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return <View />;
};
2. Optimize Dependency Arrays
// ❌ Bad: Missing dependencies
const BadComponent = ({ userId }) => {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Missing userId dependency
return <View />;
};
// ✅ Good: Correct dependencies
const GoodComponent = ({ userId }) => {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Correct dependency
return <View />;
};
3. Separate Concerns
// ✅ Good: Separate effects for different concerns
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// Effect for user data
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// Effect for posts data
useEffect(() => {
fetchUserPosts(userId).then(setPosts);
}, [userId]);
// Effect for analytics
useEffect(() => {
trackUserView(userId);
}, [userId]);
return <View />;
};
Common Lifecycle Scenarios
1. Data Fetching on Mount
const DataFetchingComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <Text>Loading...</Text>;
if (error) return <Text>Error: {error}</Text>;
return <Text>{JSON.stringify(data)}</Text>;
};
2. Responding to Prop Changes
const UserDetails = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!userId) return;
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <Text>Loading user...</Text>;
if (!user) return <Text>No user found</Text>;
return (
<View>
<Text>{user.name}</Text>
<Text>{user.email}</Text>
</View>
);
};
3. Cleanup on Unmount
const WebSocketComponent = () => {
const [messages, setMessages] = useState([]);
const [socket, setSocket] = useState(null);
useEffect(() => {
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
ws.onopen = () => {
console.log('WebSocket connected');
};
ws.onclose = () => {
console.log('WebSocket disconnected');
};
setSocket(ws);
// Cleanup on unmount
return () => {
ws.close();
};
}, []);
return (
<View>
{messages.map((message, index) => (
<Text key={index}>{message.text}</Text>
))}
</View>
);
};
Performance Monitoring
import { useState, useEffect } from 'react';
const usePerformanceMonitor = (componentName) => {
useEffect(() => {
const startTime = performance.now();
return () => {
const endTime = performance.now();
console.log(`${componentName} was mounted for ${endTime - startTime}ms`);
};
}, [componentName]);
};
const MonitoredComponent = () => {
usePerformanceMonitor('MonitoredComponent');
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const start = performance.now();
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
const end = performance.now();
console.log(`Data fetch took ${end - start}ms`);
} catch (error) {
console.error('Fetch error:', error);
}
};
fetchData();
}, []);
return <View>{/* Component content */}</View>;
};
Summary
Component lifecycle management is essential for building efficient React Native applications. Key takeaways:
- Class Components: Use lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount
- Functional Components: Use useEffect hook to handle all lifecycle events
- Cleanup: Always clean up subscriptions, timers, and event listeners
- Performance: Use React.memo and proper dependency arrays to optimize re-renders
- Error Handling: Implement error boundaries for better error management
- Best Practices: Separate concerns, avoid common mistakes, and monitor performance
Understanding lifecycle methods helps you write more predictable, performant, and maintainable React Native applications. In the next chapter, we’ll explore State Management Basics in React Native, building upon the lifecycle concepts covered here.
Related Articles:
- React Native vs Flutter in 2024: A Detailed Comparison
- Top 10 React Native Libraries You Must Know in 2024
- React vs React Native: A Deep Dive into Key Differences
- How to Set Up Navigation in React Native : Step-by-Step Guide
- What is Axios? Fetch vs Axios: What’s the Difference?
- React Native Environment Setup: A Beginner’s Step-by-Step Guide
- React Native Web: A Step-by-Step Guide to Cross-Platform Development