Side Effects and Lifecycle

What are Side Effects ?

React components are intended to be pure. This means if we give a function the same Props, it should return the exact same JSX, and it shouldn't change anything outside itself.

However, real apps need to talk to the outside world. These actions are called Side Effects:

  • Fetching data from an API.
  • Setting up a subscription (e.g., chat socket).
  • Manually changing the DOM (e.g., document.title = ...).
  • Setting timers (setTimeout).

Rule: Never perform side effects directly inside the main component body (during render). Use useEffect.


The useEffect Hook

useEffect tells React that our component needs to do something after the render is committed to the screen.

Syntax

import { useEffect } from "react";
 
function UserProfile({ userId }) {
  useEffect(() => {
    // This code runs AFTER the component renders/updates
    console.log("Component rendered!");
 
    // Example: Fetch data
    fetchData(userId);
  });
 
  return <div>User Profile</div>;
}

The Dependency Array

The most important part of useEffect is the second argument: the Dependency Array []. It controls when the effect runs.

Array ContentBehaviorLifecycle Equivalent
No ArrayRuns on every render. (Dangerous loop risk!)componentDidUpdate (always)
Empty Array []Runs only once on mount.componentDidMount
Variables [data]Runs on mount + whenever data changes.componentDidUpdate (conditional)

Examples:

Case A: Run Once (Mount)

useEffect(() => {
  console.log("I run only when the component first appears.");
}, []); // Empty array

Case B: Run on Change

useEffect(() => {
  console.log("I run when 'count' changes.");
}, [count]); // Dependency array contains 'count'

The Cleanup Function

Sometimes an effect creates a mess that needs cleaning up when the component is removed.

  • Examples: Clearing a timer, closing a WebSocket connection, removing an event listener.

To do this, we return a function from our effect.

useEffect(() => {
  // 1. Setup
  const timer = setInterval(() => {
    console.log("Tick...");
  }, 1000);
 
  // 2. Cleanup (Runs when component unmounts OR before re-running effect)
  return () => {
    clearInterval(timer);
    console.log("Timer cleared!");
  };
}, []);

If we forget this, our app will have memory leaks (ghost timers running in the background).


Lifecycle Comparison: Class vs. Hooks

Class MethodFunctional Hook Equivalent
componentDidMountuseEffect(() => { ... }, [])
componentDidUpdateuseEffect(() => { ... }, [prop, state])
componentWillUnmountuseEffect(() => { return () => { ... } }, [])

📝 Summary Table

ConceptDefinitionKey Rule
Side EffectInteractions with the outside world (API, DOM).Never do this in the render body.
useEffectHook to handle side effects.Runs after the browser paints.
Dependency ArrayControls when the effect runs.Always include variables we use inside.
Cleanup FunctionReturn function from useEffect.Prevents memory leaks on unmount.

🛑 Stop and Think

1. What happens if we modify a state variable inside a useEffect (e.g., setCount(count + 1)) but forget to add the dependency array []?

We create an Infinite Loop that will crash our browser tab.

  • The Cycle:
    1. Component Renders.
    2. useEffect runs (because there is no dependency array, it runs after every render).
    3. setCount updates the state.
    4. State change triggers a Re-render.
    5. Go back to Step 1.
  • The Fix: Always use a dependency array or check a condition before setting state.

2. In React 18 Strict Mode (development), useEffect runs twice on mount. Why does React do this intentionally?

React is stress-testing our Cleanup Function.

  • The Sequence: React does: MountUnmountMount.
  • The Goal: If our effect creates a subscription (like a chat connection) but forgets the cleanup function, we will end up with two connections open. By forcibly running this cycle, React makes the bug obvious (e.g., we see "Connected" printed twice in the console) so we fix it before production.
  • Note: This only happens in Development, not Production.