Hello! Welcome to your fourth lesson in the "React Fundamentals Refresher" module.
In our last session, we took a deep dive into useEffect and the critical importance of its dependency array. We established that useEffect is for synchronizing your component with external systems and that you must always declare all reactive dependencies. We briefly touched on the cleanup function as a way to handle race conditions in data fetching and to fix a setInterval bug.
Today, we will focus exclusively on that cleanup function. You'll learn why it is your primary tool for preventing a critical class of bugs known as memory leaks. We will implement cleanup logic for common side effects you'll encounter in React Native development, such as timers and event listeners. Mastering this concept is fundamental to building stable, performant applications—a core part of your goal to understand React Native's under-the-hood mechanics.
What is a Memory Leak?
A memory leak occurs when an application allocates memory for a resource (like an object, a timer, or a subscription) but fails to release it when it's no longer needed. Over time, this accumulated, unused memory can consume system resources, leading to degraded performance, sluggishness, and eventually, application crashes.

In the context of mobile apps, which often run for extended periods without being fully closed, even small, recurring memory leaks can be fatal. Unlike a browser tab that you might close after a few minutes, a React Native app is expected to run reliably for hours or days.
Common causes of memory leaks in React and React Native include:
- Timers (
setIntervalorsetTimeout) that are not cleared. - Event listeners (e.g., for device orientation, keyboard status, or back button presses) that are not removed.
- WebSocket connections that are not closed.
- Subscriptions to external data sources that are not cancelled.
The useEffect Cleanup Mechanism
As we saw previously, the function you pass to useEffect can optionally return another function. This returned function is the cleanup function.
useEffect(() => {
// 1. The effect's setup logic runs here.
// (e.g., add event listener, start timer)
// 2. Return a function to handle cleanup.
return () => {
// 3. The cleanup logic runs here.
// (e.g., remove event listener, clear timer)
};
}, [/* dependencies */]);
React executes this cleanup function at two specific times:
- Before re-running the effect: If a dependency changes, React runs the cleanup from the previous render before executing the effect for the current render. This prevents multiple instances of the same resource from running simultaneously.
- When the component unmounts: When the component is removed from the UI, React runs the cleanup from the final render to free up all resources.
A key point for understanding "under-the-hood" behavior is that in Development Mode, React intentionally mounts, unmounts, and then re-mounts your components on their initial render. It does this specifically to help you find bugs related to missing cleanup logic. If you see your effect running twice on mount, it's often React's way of telling you to double-check your cleanup.
Let's watch a video that introduces this concept and demonstrates this Dev Mode behavior.
useEffect Hook - React In Depth
The video 'useEffect Hook - React In Depth' provides a great explanation of the cleanup function's purpose and timing. It also clearly shows the mount-unmount-remount cycle that React performs in development mode.
Watch from 35:21 to 40:01. Focus on: The concept of returning a function from useEffect to act as a cleanup mechanism. How the console logs 'mounted', 'unmounted', and then 'mounted' again, illustrating React's strict mode behavior for testing effects.
Practical Cleanup: Timers and setInterval
Timers are a classic source of memory leaks. If you start an interval when a component mounts but forget to clear it on unmount, the interval will continue to run in the background indefinitely, consuming CPU cycles and memory, even after the component is gone.
The following video segment provides an excellent, in-depth demonstration of this exact problem and its solution. It shows how multiple intervals can be created accidentally, leading to a rapidly escalating memory leak, and how a simple cleanup function completely solves the issue.
useEffect Hook - React In Depth
Let's continue with the same video, 'useEffect Hook - React In Depth'. This segment provides a powerful, practical demonstration of a memory leak caused by setInterval and the correct way to clean it up.
Watch from 44:45 to 55:44. This is a very detailed walkthrough. Pay close attention to: How, without cleanup, clicking a button that causes a re-render creates more and more intervals, which you can see spamming the console. How setInterval returns an ID. How the cleanup function uses clearInterval(id) to stop the timer from the previous render before a new one is created or when the component unmounts.
Practical Cleanup: Event Listeners
Another common side effect is adding an event listener to a global object, such as listening for keyboard events or device dimension changes in React Native. Just like timers, these listeners must be removed when the component unmounts to prevent errors and memory leaks. If not removed, the listener's callback function might try to update the state of a component that no longer exists.
This next short video clearly illustrates the problem and solution with a simple mousemove event listener. The principle is identical for React Native listeners.
React Hooks Tutorial - 10 - useEffect with cleanup
The video 'useEffect with cleanup' from Codevolution offers a concise and effective demonstration of cleaning up an event listener.
Watch from 01:55 to 05:47. Notice: The 'Can't perform a React state update on an unmounted component' warning that appears when the listener is not cleaned up. How returning a function that calls removeEventListener from the useEffect hook resolves the issue.
In React Native, you would apply the exact same pattern for listeners like AppState (to detect if the app is in the foreground or background) or the BackHandler on Android.
The following article provides code snippets showing this pattern in a React Native context.
Detecting and Handling Memory Leaks in React Native Apps
Let's look at the article 'Detecting and Handling Memory Leaks in React Native Apps'. It provides concrete React Native examples that reinforce the cleanup pattern we've been discussing.
Skim through the sections titled 'Part 5: Proper Component Cleanup and Unmounting' and 'Part 6: Effective Management of Event Listeners and Subscriptions'. You don't need to read in detail, just observe the code examples. Note how they use useEffect to add listeners for hardwareBackPress and AppState changes, and critically, how they return a cleanup function to remove those same listeners.
A Comprehensive Example
Real-world components can be complex, often managing multiple side effects at once. A single useEffect hook can set up several resources, and its cleanup function should be responsible for tearing all of them down.
This final reading shows a realistic ChatScreen component that manages keyboard event listeners, a setInterval for a typing indicator, a WebSocket connection, and an async data fetch. It's a great example of how a single, comprehensive cleanup function can prevent multiple potential memory leaks.
React Native performance tactics: Modern strategies and tools
The Sentry blog post on React Native performance provides an excellent 'bad vs. good' example that ties everything together.
Read the section titled 'Fix JavaScript memory leaks'. First, examine the 'Bad' code block and see if you can spot the four distinct memory leaks. Then, study the 'Good' code block and pay close attention to the return function within useEffect. Notice how it meticulously cleans up every single resource that was created: the event listeners are removed, the interval is cleared, and the WebSocket is closed.
Key Takeaways
- Memory leaks occur when resources are allocated but not released. They are especially critical in long-running mobile apps.
- The function returned from
useEffectis React's dedicated cleanup mechanism. - The cleanup function runs before the next effect (on re-render) and on component unmount.
- The golden rule of cleanup is: always clean up what you set up. Clear timers, remove listeners, and close connections to prevent leaks and bugs.
- React's Dev Mode behavior (mount-unmount-remount) is designed to help you catch missing cleanup logic early.
You've now solidified your understanding of useEffect's two main parts: synchronizing with the outside world and cleaning up after yourself. This knowledge is crucial for writing the kind of robust, professional code required for high-quality applications.
In our next lesson, we will shift from preventing bugs and memory leaks to actively improving rendering performance. We'll explore useMemo and useCallback, two hooks that help you prevent unnecessary re-computations and re-renders, further deepening your understanding of React's performance characteristics.