DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 13: React Native Lifecycle Methods and Component Lifecycle

React Native Lifecycle Methods

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:

  1. Mounting: Component is being created and inserted into the DOM
  2. Updating: Component is being re-rendered as a result of changes to props or state
  3. 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:

Scroll to Top