React

Chapter 07: useEffect & Lifecycle β€” Side effects, dependency array rules, cleanup functions, memory leaks, and React 18 StrictMode double-mounting.

1. What is a Side Effect?

In React, component render functions should ideally be pure: they take props and state, calculate JSX, and return it without modifying anything outside the component. Any operation that interacts with the outside world is called a Side Effect.

πŸ’‘ Real-World Analogy: Chef Cooking vs Delivering Food

Think of your component lifecycle:

  • Pure Render Phase (Chef in Kitchen): Preparing the dish on a plate based solely on the recipe ticket (props & state). Zero outside interaction.
  • Side Effect (Delivery Driver): Leaving the kitchen to deliver food to the customer's house, making phone calls, or updating inventory logs (fetching data from APIs, setting timers, attaching window resize listeners, modifying document.title).

2. The 3 Dependency Array Modes

The behavior of useEffect(callback, dependencies) is entirely dictated by its second argumentβ€”the dependency array:

01

No Dependency Array

Runs after EVERY render

Executes on initial mount and after every single state/prop change. Rare; often causes infinite loops if you update state inside!

useEffect(() => {
    console.log("Rendered!");
});
02

Empty Array []

Runs ONCE on Mount

Executes only once when the component is first inserted into the DOM. Perfect for initial API calls and global event setup.

useEffect(() => {
    fetchInitialData();
}, []);
03

With Dependencies [a, b]

Runs on Mount + Changes

Executes on mount, and re-executes only if any value in the dependency array changes (checked via Object.is()).

useEffect(() => {
    fetchUser(userId);
}, [userId]);

3. The Cleanup Function (Preventing Memory Leaks)

If your effect creates an ongoing subscription, starts an interval/timer, or attaches an event listener to window, you must return a cleanup function. React calls this cleanup function right before the component unmounts, and before re-running the effect on dependency change.

function LiveClock() {
    const [seconds, setSeconds] = React.useState(0);

    React.useEffect(() => {
        // 1. Setup Phase: Start the interval timer
        const timerId = setInterval(() => {
            setSeconds(prev => prev + 1);
        }, 1000);

        // 2. Cleanup Phase: Executed when component unmounts
        return () => {
            clearInterval(timerId); // πŸ›‘ Prevents orphan timers in memory!
            console.log("Timer cleaned up safely!");
        };
    }, []); // Empty array = mount & unmount lifecycle

    return <div>Elapsed: {seconds}s</div>;
}
Lifecycle Console Output:
[Mount] LiveClock inserted into DOM -> setInterval #1 started [Tick] Elapsed: 1s [Tick] Elapsed: 2s [Unmount] Component removed from DOM -> clearInterval(#1) executed!

4. Top Interview Questions & Common Pitfalls

⚠️ Pitfall 1: "Why can't I write useEffect(async () => { ... }) directly?"

The Explanation: An async function automatically returns a Promise. But React expects the return value of useEffect to either be undefined or a cleanup function! If you return a Promise, React cannot clean up the effect.

// ❌ WRONG: Async effect callback
// useEffect(async () => { const res = await fetch(...); }, []);

// βœ… CORRECT: Declare an inner async function and invoke it immediately:
useEffect(() => {
    async function loadData() {
        const res = await fetch("/api/users");
        const json = await res.json();
        setUsers(json);
    }
    loadData();
}, []);
Async Fetch Execution Preview:
[useEffect] Initiated loadData() async request... [Network] GET /api/users HTTP/2 200 OK (38ms) [React] setUsers([...]) triggered component state update!

⚠️ Pitfall 2: "Why does my useEffect run twice on mount in React 18?"

Senior Answer: In React 18 development mode inside <React.StrictMode>, React intentionally mounts, unmounts, and re-mounts your components immediately (Mount $\rightarrow$ Unmount $\rightarrow$ Remount). This stress-tests your code to verify that your cleanup functions are properly implemented and don't leak subscriptions!

5. Hands-On Practice Challenges

Master component lifecycles, dependency diffing, and cleanup handlers with these interactive challenges. Click "Solve Challenge" to open the code editor!

🎯 Hands-on Coding Challenge Dependency Diffing

Challenge 1: Replicate useEffect Dependency Diffing

Write a utility simulateEffectRunner(effectFn, deps, prevDeps) that compares current dependencies with previous dependencies using Object.is() and returns whether the effect should run.

πŸ“‹ Requirements & What to Write:
  • If prevDeps === null, return { shouldRun: true, reason: 'INITIAL_MOUNT' }.
  • Compare elements using Object.is(deps[i], prevDeps[i]).
  • If any item differs, return shouldRun: true. If all match, return shouldRun: false.
Expected Result: { shouldRun: true, reason: "INITIAL_MOUNT" } | { shouldRun: false, reason: "DEPS_IDENTICAL" }
πŸ’‘
Editor Instructions: Complete simulateEffectRunner to implement dependency equality checks.
effectRunner.js
Console / Terminal Output
// Output will appear here when you click "Run Code"...
🎯 Hands-on Coding Challenge Cleanup & Memory Leaks

Challenge 2: Effect Cleanup & Subscription Lifecycle

Implement a createSubscriptionManager() closure that simulates subscribing to window events and returning a cleanup un-subscriber function to prevent listener accumulation.

πŸ“‹ Requirements & What to Write:
  • Track activeListeners count in closure.
  • subscribe(eventName) increments counter and returns a cleanup callback.
  • Executing the returned cleanup callback decrements the counter.
Expected Result: Active before cleanup: 2 | Active after unsubA: 1
πŸ’‘
Editor Instructions: Return an unsubscribe function inside subscribe.
subscriptionManager.js
Console / Terminal Output
// Output will appear here when you click "Run Code"...
🎯 Hands-on Coding Challenge Linter & Rule Auditor

Challenge 3: useEffect Linter & Memory Leak Detector

Write a linter function checkEffectBestPractices(effectCodeString) that scans effect code and flags direct async callbacks and missing cleanup returns on timers.

πŸ“‹ Requirements & What to Write:
  • Flag direct async callbacks (e.g. useEffect(async () => ...)).
  • Flag missing return cleanup when setInterval or addEventListener are used.
  • Return 'βœ… Effect adheres to best practices!' if clean.
Expected Result: Audit Buggy: ["CRITICAL: useEffect callback cannot be async directly.", ...] | Audit Good: "βœ… Effect adheres to best practices!"
πŸ’‘
Editor Instructions: Complete checkEffectBestPractices and test against sample code strings.
effectLinter.js
Console / Terminal Output
// Output will appear here when you click "Run Code"...