useEffect is the designated place for all side effects in React. But what does that mean? How does it decide when to run our code, and what is the dependency array really doing? Just like useState, useEffect isn't magic; it's a predictable system with clear rules.
Let's dive into the internal mechanics of useEffect.
The Problem: Side Effects in a Declarative World
React's rendering model is designed to be declarative and pure: the UI should be a direct function of the current state (UI = f(state)). But applications need to interact with the outside world—fetching data, setting timers, subscribing to events. These are "side effects," and they don't fit neatly into the pure rendering model.
useEffect provides a safe, controlled "escape hatch." It allows us to run imperative, effectful code without disrupting the rendering cycle.
  
  
  The useEffect Lifecycle: It Runs After the Render
The most critical concept to understand is that useEffect does not run during the render. Running a side effect directly in the component body is a common mistake, as it can block the UI from updating.
Instead, useEffect schedules your effect function to be executed after React has committed the changes to the DOM and the browser has painted the screen. This ensures that user-facing updates are never delayed by a slow network request or a complex subscription setup.
Here's the flow:
-  You trigger a re-render (e.g., by calling setState).
- React calculates the new UI.
- React commits the changes to the DOM.
- The browser paints the updated UI to the screen.
-  Only now, React runs the functions you passed to useEffect.
The Dependency Array: React's Memory Game
This is the heart of useEffect's logic. How does React know whether to re-run your effect on a subsequent render? It plays a memory game using the dependency array.
The Technical Idea:
Just like with useState, React maintains a "memory cell" for each useEffect call within your component's internal data structure. This cell stores the dependency array from the previous render.
- Initial Render: React runs your effect function. It then stores the dependency array you provided (e.g., - [userId]) in the hook's memory cell.
- Subsequent Renders: When the component re-renders, React gets the new dependency array from the - useEffectcall. It then compares the new array to the one it stored from the previous render.
- The Comparison: React loops through the arrays and compares each value using the - Object.is()algorithm. If any single value is different between the old and new array (- !Object.is(oldValue, newValue)), React considers the dependencies to have changed.
- 
The Decision: - If the dependencies have changed, React runs the effect again and stores the new dependency array for the next comparison.
- If all dependencies are identical, React does nothing and skips running the effect.
 
This is why passing objects or functions directly in the dependency array can be problematic. If you create a new object ({}) or function (() => {}) during the render, it will have a new reference on every render, causing the effect to run unnecessarily. You must memoize them with useMemo or useCallback to ensure a stable reference.
Special Cases:
-   Empty Array []: Since the array is always[]and never changes, the comparison always passes, and the effect runs only once after the initial render.
- No Array Provided: If you omit the dependency array, React effectively considers the dependencies to have always changed, so the effect runs after every single render.
The Cleanup Function: Preventing Memory Leaks
Side effects often create things that need to be explicitly destroyed, like timers, event listeners, or WebSocket connections. If you don't clean them up, they can cause memory leaks and bugs.
The Technical Idea:
When you return a function from your effect, React stores this "cleanup function" in the hook's memory cell.
useEffect(() => {
  const handleScroll = () => console.log('scrolled');
  window.addEventListener('scroll', handleScroll);
  // React stores this cleanup function
  return () => {
    window.removeEventListener('scroll', handleScroll);
  };
}, []);
React will execute this cleanup function in two scenarios:
- Before the Component Unmounts: When the component is removed from the UI, React runs the cleanup to ensure no lingering effects are left behind. 
- Before the Effect Re-runs: This is a crucial point. If a dependency changes and the effect needs to run again, React first runs the cleanup function from the previous render. This cleans up the old effect before setting up the new one, preventing you from having multiple, redundant listeners or subscriptions active at the same time. 
By understanding these three pillars—post-render execution, the dependency comparison, and the cleanup lifecycle—you can use useEffect to its full potential, creating predictable and robust components.
 

 
    
Top comments (2)
Very well explained! 👏🏻Keep going!
UseEffect has been a little confusing for me. But you explained it well. Thanks for the write up🙌