Skip to main content

โšก The useEffect Hook

So far, your React components have been pure functionsโ€”they take props and state, and they render JSX. But real applications need to do more than just render. They need to fetch data from APIs, subscribe to events, update the document title, set timers, and interact with the browser. These are called "side effects," and React's useEffect hook is how you handle them. Think of useEffect as your component's way of saying "after you render, do this extra thing." Let's master this essential hook! ๐Ÿš€

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Understand what side effects are and why they need special handling
  • Use the useEffect hook to perform side effects
  • Master dependency arrays and when effects run
  • Implement cleanup functions to prevent memory leaks
  • Understand effect execution timing
  • Handle common effect patterns (timers, subscriptions, document updates)
  • Debug effects and avoid infinite loops
  • Type effects properly with TypeScript
  • Know when to use useEffect vs other solutions

Estimated Time: 70-85 minutes

Project: Build a timer, document title updater, and event listener examples

๐Ÿ“‘ In This Lesson

๐ŸŽญ What Are Side Effects?

Before we dive into useEffect, we need to understand what side effects are and why they need special handling in React.

๐Ÿ“– Definition

Side Effect: Any operation that affects something outside the scope of the function being executed. In React, this means anything beyond calculating and returning JSX.

Pure Functions vs Side Effects

Understanding Purity

// โœ… PURE Function (no side effects)
function add(a: number, b: number): number {
    return a + b; // Only returns a value, no external changes
}

// โœ… PURE React Component
const Greeting: React.FC<{ name: string }> = ({ name }) => {
    return <h1>Hello, {name}!</h1>; // Only renders JSX
};

// โŒ IMPURE (has side effects)
function addAndLog(a: number, b: number): number {
    console.log('Adding numbers'); // Side effect: logging
    return a + b;
}

// โŒ IMPURE React Component (DON'T DO THIS)
const BadComponent: React.FC = () => {
    document.title = 'My App'; // Side effect in render!
    fetch('/api/data'); // Side effect in render!
    return <h1>Hello</h1>;
};

Common Side Effects in React

What Counts as a Side Effect?

Side Effect Type Examples Why It's a Side Effect
Data Fetching fetch(), axios.get() Network request affects external world
Subscriptions addEventListener, WebSocket Registers external listeners
Timers setTimeout, setInterval Schedules future code execution
DOM Manipulation document.title, focus() Directly modifies the DOM
Logging console.log() Outputs to console (external)
Browser APIs localStorage, navigator Interacts with browser APIs
External Libraries Google Analytics, charts Calls external code

Why Side Effects Need Special Handling

โš ๏ธ The Problem with Side Effects During Render

// โŒ BAD: Side effect during render
const BadTimer: React.FC = () => {
    const [count, setCount] = useState(0);
    
    // Problem: This runs on EVERY render!
    setInterval(() => {
        setCount(prev => prev + 1);
    }, 1000);
    // Result: Hundreds of timers created, app crashes!
    
    return <div>Count: {count}</div>;
};

// โŒ BAD: Fetch during render
const BadFetch: React.FC = () => {
    const [data, setData] = useState(null);
    
    // Problem: Fetches on every render, infinite loop!
    fetch('/api/data')
        .then(res => res.json())
        .then(setData); // Setting state triggers re-render!
    
    return <div>{data}</div>;
};

React's Rendering Model

graph TD A["๐Ÿ”„ Component Function Called"] --> B["๐Ÿ“ Calculate JSX"] B --> C["๐Ÿ“ค Return JSX"] C --> D["๐ŸŽจ React Updates DOM"] D --> E["๐Ÿ–ฅ๏ธ Browser Paints Screen"] E --> F["๐Ÿ‘๏ธ User Sees Update"] G["โšก Side Effects"] -.->|"Should happen AFTER"| D style A fill:#667eea,stroke:#5a67d8,color:#fff style G fill:#f44336,stroke:#d32f2f,color:#fff style D fill:#4CAF50,stroke:#388e3c,color:#fff

๐ŸŽฎ Interactive: Pure vs Impure Functions

Click the buttons to see the difference between pure and impure functions:

โœ… Pure Function

function add(a, b) {
  return a + b;
}

Same input โ†’ Same output, no side effects

โŒ Impure Function

let callCount = 0;
function addAndLog(a, b) {
  callCount++;
  console.log('Called!');
  return a + b;
}

Modifies external state, logs to console

Console: Ready...

The key insight: Render functions should be pure. Side effects should happen after rendering completes. That's what useEffect is for!

๐ŸŽฃ Introduction to useEffect

The useEffect hook tells React "after you finish rendering this component, run this code." It's your escape hatch from pure rendering into the world of side effects.

The Concept

useEffect(() => {
    // This code runs AFTER the component renders
    console.log('Component rendered!');
    
    // Perform side effects here:
    // - Fetch data
    // - Set up subscriptions
    // - Update document
    // - Start timers
});

When Does useEffect Run?

๐Ÿ’ก Effect Timing

graph LR
    A[Component Mounts] --> B[Render JSX]
    B --> C[React Updates DOM]
    C --> D[Browser Paints]
    D --> E[useEffect Runs]
    
    F[State Changes] --> G[Re-render]
    G --> H[React Updates DOM]
    H --> I[Browser Paints]
    I --> J[useEffect Runs Again]
    
    style E fill:#4CAF50,color:#fff
    style J fill:#4CAF50,color:#fff

Your First useEffect

Simple Example: Document Title

import { useState, useEffect } from 'react';

const PageTitle: React.FC = () => {
    const [count, setCount] = useState(0);
    
    // โœ… Effect runs after every render
    useEffect(() => {
        document.title = `Count: ${count}`;
        console.log('Effect ran!');
    });
    
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>
                Increment
            </button>
        </div>
    );
};

// What happens:
// 1. Component renders with count = 0
// 2. Effect runs, sets title to "Count: 0"
// 3. User clicks button
// 4. Component re-renders with count = 1
// 5. Effect runs again, sets title to "Count: 1"

Effect Flow Diagram

sequenceDiagram
    participant U as User
    participant C as Component
    participant R as React
    participant E as useEffect
    participant B as Browser
    
    U->>C: Open page
    C->>R: Render JSX (count=0)
    R->>B: Update DOM
    B->>B: Paint screen
    B->>E: Trigger effect
    E->>B: Set document.title
    
    U->>C: Click button
    C->>C: setCount(1)
    C->>R: Re-render JSX (count=1)
    R->>B: Update DOM
    B->>B: Paint screen
    B->>E: Trigger effect again
    E->>B: Set document.title again

Importing useEffect

Where It Comes From

// Import from React
import { useEffect } from 'react';

// Or with other hooks
import { useState, useEffect } from 'react';

// It's a hook, so:
// โœ… Call it at the top level of your component
// โŒ Don't call it inside loops, conditions, or nested functions

๐Ÿ“ Basic useEffect Syntax

Let's break down the anatomy of useEffect and understand each part.

The Complete Syntax

Full Form

useEffect(
    // 1. Effect function (required)
    () => {
        // Side effect code goes here
        console.log('Effect running');
        
        // 2. Cleanup function (optional)
        return () => {
            console.log('Cleanup running');
        };
    },
    // 3. Dependency array (optional but important!)
    [dependency1, dependency2]
);

Part 1: The Effect Function

What Goes Inside?

useEffect(() => {
    // This is the effect function
    // Put your side effects here
    
    // Examples:
    console.log('Component rendered');
    document.title = 'My App';
    fetch('/api/data');
    const timer = setTimeout(() => {}, 1000);
    
    // Any code that should run after render
});

Effect Function Examples

// Example 1: Update document title
useEffect(() => {
    document.title = `You clicked ${count} times`;
});

// Example 2: Log when component renders
useEffect(() => {
    console.log('Component rendered with props:', props);
});

// Example 3: Focus an input
useEffect(() => {
    inputRef.current?.focus();
});

// Example 4: Set up analytics
useEffect(() => {
    analytics.track('Page viewed', { page: 'Home' });
});

Part 2: The Cleanup Function (Optional)

Returning a Cleanup Function

useEffect(() => {
    // Set up
    const timer = setInterval(() => {
        console.log('Tick');
    }, 1000);
    
    // Cleanup (optional but often needed)
    return () => {
        clearInterval(timer);
        console.log('Timer cleaned up');
    };
});

// When does cleanup run?
// 1. Before the effect runs again (on re-render)
// 2. When the component unmounts (is removed)

โœ… Why Cleanup Matters

// โŒ WITHOUT cleanup: Memory leak!
useEffect(() => {
    const timer = setInterval(() => {
        console.log('Tick');
    }, 1000);
    // Timer keeps running even after component unmounts!
});

// โœ… WITH cleanup: Proper resource management
useEffect(() => {
    const timer = setInterval(() => {
        console.log('Tick');
    }, 1000);
    
    return () => {
        clearInterval(timer); // Stop timer when done
    };
});

Part 3: The Dependency Array (Critical!)

Three Ways to Use Dependencies

// 1. No dependency array: Runs after EVERY render
useEffect(() => {
    console.log('Runs on every render');
}); // โš ๏ธ Usually not what you want!

// 2. Empty dependency array: Runs ONCE on mount
useEffect(() => {
    console.log('Runs once when component mounts');
}, []); // โœ… Common pattern

// 3. With dependencies: Runs when dependencies change
useEffect(() => {
    console.log('Runs when count changes');
}, [count]); // โœ… Most common pattern

Visualizing Dependency Behavior

flowchart TD A["๐Ÿ”„ Component renders"] --> B{"Dependency array?"} B -->|"No array"| C["๐Ÿ” Run effect on EVERY render"] B -->|"Empty []"| D{"Is this the first render?"} D -->|"Yes"| E["โœ… Run effect"] D -->|"No"| F["โญ๏ธ Skip effect"] B -->|"With deps [x, y]"| G{"Did any dependency change?"} G -->|"Yes"| H["โœ… Run effect"] G -->|"No"| I["โญ๏ธ Skip effect"] style C fill:#f44336,stroke:#d32f2f,color:#fff style E fill:#4caf50,stroke:#388e3c,color:#fff style H fill:#4caf50,stroke:#388e3c,color:#fff style F fill:#9e9e9e,stroke:#757575,color:#fff style I fill:#9e9e9e,stroke:#757575,color:#fff

๐ŸŽฎ Interactive: Dependency Array Simulator

Simulate component renders and see when your effect runs based on different dependency configurations:

0
Render History
Effect Execution Log

TypeScript with useEffect

Typing Effects

import { useEffect } from 'react';

// Effect function doesn't return anything (except cleanup)
useEffect(() => {
    // TypeScript infers this returns void
    console.log('Hello');
});

// Cleanup function must return void
useEffect(() => {
    const timer = setInterval(() => {}, 1000);
    
    // โœ… Correct: returns void
    return () => {
        clearInterval(timer);
    };
    
    // โŒ Wrong: can't return other values
    // return 42; // TypeScript error!
});

// Dependencies must be array
useEffect(() => {
    console.log(count);
}, [count]); // โœ… Array of dependencies

// Common TypeScript pattern
interface Props {
    userId: string;
}

const UserProfile: React.FC<Props> = ({ userId }) => {
    useEffect(() => {
        // userId is properly typed as string
        console.log(`Fetching data for user: ${userId}`);
    }, [userId]); // TypeScript checks userId is in scope
    
    return <div>User Profile</div>;
};

๐ŸŽฏ Dependency Arrays Explained

The dependency array is the most important and confusing part of useEffect. Let's master it completely!

How Dependencies Work

The Rule

React compares each dependency with its previous value. If any dependency changed, the effect runs.

const [count, setCount] = useState(0);
const [name, setName] = useState('Alice');

useEffect(() => {
    console.log(`Count: ${count}, Name: ${name}`);
}, [count, name]);

// Scenario 1: count changes from 0 to 1
// Result: Effect runs โœ…

// Scenario 2: name changes from 'Alice' to 'Bob'
// Result: Effect runs โœ…

// Scenario 3: Neither changes
// Result: Effect doesn't run โŒ

// Scenario 4: Both change
// Result: Effect runs once โœ… (not twice!)

Three Dependency Patterns

Pattern 1: No Dependency Array

// Runs after EVERY render
useEffect(() => {
    console.log('Every render');
}); // No second argument

// Use case: Rarely needed
// Usually indicates you should use a dependency array instead

Pattern 2: Empty Dependency Array

// Runs ONCE when component mounts
useEffect(() => {
    console.log('Component mounted');
    
    return () => {
        console.log('Component unmounted');
    };
}, []); // Empty array

// Use cases:
// - Initialize data on mount
// - Set up subscriptions
// - Start timers
// - Fetch initial data

// Example: Fetch data once
useEffect(() => {
    fetch('/api/user')
        .then(res => res.json())
        .then(data => setUser(data));
}, []); // Runs once on mount

Pattern 3: With Dependencies

// Runs when specified values change
useEffect(() => {
    console.log(`User ${userId} selected`);
    fetchUserData(userId);
}, [userId]); // Runs when userId changes

// Use cases:
// - Sync with prop changes
// - React to state changes
// - Re-fetch when parameters change

// Example: Search as user types
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);

useEffect(() => {
    if (searchTerm) {
        fetch(`/api/search?q=${searchTerm}`)
            .then(res => res.json())
            .then(data => setResults(data));
    }
}, [searchTerm]); // Re-run when search term changes

What Should Go in Dependencies?

โœ… The Golden Rule

Every value from the component scope that's used inside the effect MUST be in the dependency array.

const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(2);

// โœ… CORRECT: Both count and multiplier are dependencies
useEffect(() => {
    const result = count * multiplier;
    console.log(`Result: ${result}`);
}, [count, multiplier]);

// โŒ WRONG: Missing multiplier from dependencies
useEffect(() => {
    const result = count * multiplier; // Uses multiplier
    console.log(`Result: ${result}`);
}, [count]); // But doesn't list it!
// This will use stale values of multiplier!

Common Dependency Mistakes

โš ๏ธ Watch Out For These

// โŒ Mistake 1: Forgetting dependencies
const [userId, setUserId] = useState(1);

useEffect(() => {
    fetchUser(userId); // Uses userId
}, []); // But empty array! Will always fetch user 1

// โœ… Fix: Include userId
useEffect(() => {
    fetchUser(userId);
}, [userId]);

// โŒ Mistake 2: Including functions that change every render
const handleClick = () => {
    console.log('Clicked');
};

useEffect(() => {
    document.addEventListener('click', handleClick);
}, [handleClick]); // handleClick is new every render!

// โœ… Fix: Define function inside effect
useEffect(() => {
    const handleClick = () => {
        console.log('Clicked');
    };
    document.addEventListener('click', handleClick);
}, []);

// โŒ Mistake 3: Object dependencies
const config = { theme: 'dark', lang: 'en' };

useEffect(() => {
    applyConfig(config);
}, [config]); // config is new object every render!

// โœ… Fix: Depend on individual properties
useEffect(() => {
    applyConfig({ theme: 'dark', lang: 'en' });
}, []); // Or use theme and lang as separate dependencies

Reference vs Value Dependencies

Understanding Comparison

// Primitives: Compared by value
const [count, setCount] = useState(0);
useEffect(() => {
    console.log(count);
}, [count]); // โœ… count compared by value (0 === 0)

// Objects: Compared by reference
const [user, setUser] = useState({ name: 'Alice' });
useEffect(() => {
    console.log(user.name);
}, [user]); // โš ๏ธ user compared by reference (may cause extra runs)

// Better: Depend on specific properties
useEffect(() => {
    console.log(user.name);
}, [user.name]); // โœ… Compare only name property

// Arrays: Also compared by reference
const [items, setItems] = useState([1, 2, 3]);
useEffect(() => {
    console.log('Items changed');
}, [items]); // New array reference triggers effect

// Functions: Always new reference
const handleSubmit = () => {
    console.log('Submit');
};
useEffect(() => {
    // ...
}, [handleSubmit]); // โš ๏ธ Different function every render!

ESLint Rule: exhaustive-deps

๐Ÿ’ก Let ESLint Help You

React provides an ESLint rule that automatically checks your dependencies:

# Install if not already installed
npm install eslint-plugin-react-hooks --save-dev
// .eslintrc.json
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/exhaustive-deps": "warn"
  }
}

This rule will warn you when:

  • You're missing dependencies
  • You have unnecessary dependencies
  • Your effect might cause infinite loops

๐Ÿงน Cleanup Functions

Cleanup functions are essential for preventing memory leaks and ensuring your effects don't cause problems when components unmount or re-render.

Why Cleanup Is Needed

The Problem

// โŒ WITHOUT cleanup
const Timer: React.FC = () => {
    const [seconds, setSeconds] = useState(0);
    
    useEffect(() => {
        setInterval(() => {
            setSeconds(prev => prev + 1);
        }, 1000);
    }, []);
    
    return <div>Seconds: {seconds}</div>;
};

// Problem: When component unmounts, interval keeps running!
// setSeconds tries to update non-existent component
// Memory leak! ๐Ÿ’ฅ

โœ… WITH Cleanup

const Timer: React.FC = () => {
    const [seconds, setSeconds] = useState(0);
    
    useEffect(() => {
        const intervalId = setInterval(() => {
            setSeconds(prev => prev + 1);
        }, 1000);
        
        // Cleanup function
        return () => {
            clearInterval(intervalId);
            console.log('Timer cleaned up');
        };
    }, []);
    
    return <div>Seconds: {seconds}</div>;
};

// Now: When component unmounts, interval is cleared โœ…
// No memory leak! ๐ŸŽ‰

When Cleanup Runs

sequenceDiagram participant C as Component participant E as Effect participant Cl as Cleanup Note over C,Cl: First Mount C->>E: Run effect Note over C,Cl: Dependency Changes E->>Cl: Run cleanup first Cl->>E: Then run effect again Note over C,Cl: Component Unmounts E->>Cl: Run cleanup Cl->>C: Component removed

๐ŸŽฎ Interactive: Effect & Cleanup Lifecycle

Watch how effects and cleanup functions run during the component lifecycle:

Component Status: Not Mounted
Timeline: Click "Mount Component" to start...

Cleanup Timing Example

const EffectLifecycle: React.FC = () => {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        console.log(`Effect running for count: ${count}`);
        
        return () => {
            console.log(`Cleanup running for count: ${count}`);
        };
    }, [count]);
    
    return (
        <button onClick={() => setCount(count + 1)}>
            Count: {count}
        </button>
    );
};

// Console output:
// Mount: "Effect running for count: 0"
// Click: "Cleanup running for count: 0"
//        "Effect running for count: 1"
// Click: "Cleanup running for count: 1"
//        "Effect running for count: 2"
// Unmount: "Cleanup running for count: 2"

Common Cleanup Scenarios

1. Timers

// setTimeout
useEffect(() => {
    const timeoutId = setTimeout(() => {
        console.log('Delayed action');
    }, 3000);
    
    return () => {
        clearTimeout(timeoutId);
    };
}, []);

// setInterval
useEffect(() => {
    const intervalId = setInterval(() => {
        console.log('Repeating action');
    }, 1000);
    
    return () => {
        clearInterval(intervalId);
    };
}, []);

2. Event Listeners

useEffect(() => {
    const handleResize = () => {
        console.log('Window resized');
    };
    
    window.addEventListener('resize', handleResize);
    
    return () => {
        window.removeEventListener('resize', handleResize);
    };
}, []);

// Mouse events
useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
        console.log(`Mouse: ${e.clientX}, ${e.clientY}`);
    };
    
    document.addEventListener('mousemove', handleMouseMove);
    
    return () => {
        document.removeEventListener('mousemove', handleMouseMove);
    };
}, []);

3. Subscriptions

// WebSocket
useEffect(() => {
    const ws = new WebSocket('ws://localhost:8080');
    
    ws.onmessage = (event) => {
        console.log('Message:', event.data);
    };
    
    return () => {
        ws.close();
        console.log('WebSocket closed');
    };
}, []);

// EventSource (Server-Sent Events)
useEffect(() => {
    const eventSource = new EventSource('/api/events');
    
    eventSource.onmessage = (event) => {
        console.log('Event:', event.data);
    };
    
    return () => {
        eventSource.close();
    };
}, []);

4. Async Operations

useEffect(() => {
    let cancelled = false;
    
    const fetchData = async () => {
        const response = await fetch('/api/data');
        const data = await response.json();
        
        // Only update if not cancelled
        if (!cancelled) {
            setData(data);
        }
    };
    
    fetchData();
    
    return () => {
        cancelled = true; // Mark as cancelled
    };
}, []);

// AbortController (modern approach)
useEffect(() => {
    const controller = new AbortController();
    
    fetch('/api/data', { signal: controller.signal })
        .then(res => res.json())
        .then(data => setData(data))
        .catch(err => {
            if (err.name === 'AbortError') {
                console.log('Fetch aborted');
            }
        });
    
    return () => {
        controller.abort(); // Cancel fetch
    };
}, []);

โœ… Cleanup Checklist

Always clean up these resources:

  • โœ… Timers (setTimeout, setInterval)
  • โœ… Event listeners (addEventListener)
  • โœ… WebSockets and SSE connections
  • โœ… Subscriptions (RxJS, etc.)
  • โœ… Animation frames (requestAnimationFrame)
  • โœ… Async operations (fetch with AbortController)
  • โœ… Third-party library instances

โฑ๏ธ Effect Execution Timing

Understanding exactly when effects run is crucial for avoiding bugs and writing correct code.

The Render-Effect Cycle

graph TD A["๐Ÿ”„ Component Function Runs"] --> B["๐Ÿ“ Calculate JSX"] B --> C["๐Ÿ“ค Return JSX to React"] C --> D["๐Ÿ” React Compares with Previous"] D --> E["๐ŸŽจ React Updates Real DOM"] E --> F["๐Ÿ–ผ๏ธ Browser Paints Screen"] F --> G["โšก useEffect Runs"] G --> H{"Dependencies Changed?"} H -->|"Yes"| I["โ–ถ๏ธ Run Effect Again"] H -->|"No"| J["โญ๏ธ Skip Effect"] style A fill:#667eea,stroke:#5a67d8,color:#fff style G fill:#4CAF50,stroke:#388e3c,color:#fff style I fill:#FF9800,stroke:#f57c00,color:#fff style J fill:#9e9e9e,stroke:#757575,color:#fff

๐ŸŽฎ Interactive: Effect Execution Timeline

Watch the exact order of execution during a React render cycle:

// Console output will appear here...

useEffect vs useLayoutEffect

Two Types of Effects

Aspect useEffect useLayoutEffect
When It Runs After browser paint Before browser paint
Blocks Painting? No (asynchronous) Yes (synchronous)
Use Case Most side effects DOM measurements, preventing flicker
Performance Better (non-blocking) Can cause jank if slow
Default Choice โœ… Yes โŒ Only when needed

useEffect Timing (Standard)

const TimingExample: React.FC = () => {
    const [count, setCount] = useState(0);
    
    console.log('1. Component rendering, count:', count);
    
    useEffect(() => {
        console.log('3. Effect running, count:', count);
    });
    
    console.log('2. About to return JSX');
    
    return <button onClick={() => setCount(count + 1)}>{count}</button>;
};

// Console output when component mounts:
// 1. Component rendering, count: 0
// 2. About to return JSX
// 3. Effect running, count: 0

// User clicks button:
// 1. Component rendering, count: 1
// 2. About to return JSX
// 3. Effect running, count: 1

Multiple Effects Execution Order

When You Have Multiple Effects

const MultipleEffects: React.FC = () => {
    useEffect(() => {
        console.log('Effect 1');
        return () => console.log('Cleanup 1');
    }, []);
    
    useEffect(() => {
        console.log('Effect 2');
        return () => console.log('Cleanup 2');
    }, []);
    
    useEffect(() => {
        console.log('Effect 3');
        return () => console.log('Cleanup 3');
    }, []);
    
    return <div>Hello</div>;
};

// On Mount:
// Effect 1
// Effect 2
// Effect 3

// On Unmount:
// Cleanup 1
// Cleanup 2
// Cleanup 3

// Effects run in the order they're defined!

Effects and State Updates

Setting State Inside Effects

const EffectWithState: React.FC = () => {
    const [count, setCount] = useState(0);
    const [doubled, setDoubled] = useState(0);
    
    // Effect that updates state
    useEffect(() => {
        console.log('Effect: Doubling count');
        setDoubled(count * 2);
    }, [count]);
    
    return (
        <div>
            <p>Count: {count}</p>
            <p>Doubled: {doubled}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
    );
};

// Timeline when user clicks:
// 1. setCount(1) called
// 2. Component re-renders with count = 1
// 3. Effect runs, sees count changed
// 4. setDoubled(2) called
// 5. Component re-renders again with doubled = 2

โš ๏ธ Be Careful: Effects Can Trigger More Renders

// โŒ This causes infinite loop!
const InfiniteLoop: React.FC = () => {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        setCount(count + 1); // Updates state
    }); // No dependency array!
    // Effect runs after every render
    // setCount causes render
    // Render causes effect
    // Effect causes render... ๐Ÿ’ฅ
    
    return <div>{count}</div>;
};

// โœ… Fix: Add proper dependencies
useEffect(() => {
    // Only run once on mount
}, []);

Timing with Async Effects

Effects Can't Be Async Functions

// โŒ WRONG: Can't make effect function async
useEffect(async () => {
    const data = await fetch('/api/data');
    // TypeScript error: Effect must return cleanup or void
}, []);

// โœ… CORRECT: Define async function inside
useEffect(() => {
    const fetchData = async () => {
        const response = await fetch('/api/data');
        const data = await response.json();
        setData(data);
    };
    
    fetchData();
}, []);

// โœ… CORRECT: Use .then()
useEffect(() => {
    fetch('/api/data')
        .then(res => res.json())
        .then(data => setData(data))
        .catch(err => console.error(err));
}, []);

๐ŸŽจ Common Effect Patterns

Let's explore the most common patterns you'll use with useEffect in real applications.

Pattern 1: Document Title Updates

Updating the Browser Tab Title

const DocumentTitle: React.FC = () => {
    const [count, setCount] = useState(0);
    const [unread, setUnread] = useState(0);
    
    useEffect(() => {
        if (unread > 0) {
            document.title = `(${unread}) You have notifications`;
        } else {
            document.title = 'My App';
        }
    }, [unread]);
    
    return (
        <div>
            <h1>Notifications</h1>
            <p>Unread: {unread}</p>
            <button onClick={() => setUnread(unread + 1)}>
                New Notification
            </button>
        </div>
    );
};

Pattern 2: Timers and Intervals

Countdown Timer

const Countdown: React.FC<{ seconds: number }> = ({ seconds: initialSeconds }) => {
    const [seconds, setSeconds] = useState(initialSeconds);
    const [isActive, setIsActive] = useState(false);
    
    useEffect(() => {
        if (!isActive) return;
        
        if (seconds === 0) {
            setIsActive(false);
            alert('Time is up!');
            return;
        }
        
        const intervalId = setInterval(() => {
            setSeconds(prev => prev - 1);
        }, 1000);
        
        return () => clearInterval(intervalId);
    }, [seconds, isActive]);
    
    return (
        <div>
            <h2>{seconds} seconds</h2>
            <button onClick={() => setIsActive(!isActive)}>
                {isActive ? 'Pause' : 'Start'}
            </button>
            <button onClick={() => setSeconds(initialSeconds)}>
                Reset
            </button>
        </div>
    );
};

Stopwatch

const Stopwatch: React.FC = () => {
    const [time, setTime] = useState(0);
    const [isRunning, setIsRunning] = useState(false);
    
    useEffect(() => {
        if (!isRunning) return;
        
        const startTime = Date.now() - time;
        
        const intervalId = setInterval(() => {
            setTime(Date.now() - startTime);
        }, 10);
        
        return () => clearInterval(intervalId);
    }, [isRunning]);
    
    const formatTime = (ms: number) => {
        const seconds = Math.floor(ms / 1000);
        const minutes = Math.floor(seconds / 60);
        const hours = Math.floor(minutes / 60);
        
        return `${hours.toString().padStart(2, '0')}:${(minutes % 60)
            .toString()
            .padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`;
    };
    
    return (
        <div>
            <h1>{formatTime(time)}</h1>
            <button onClick={() => setIsRunning(!isRunning)}>
                {isRunning ? 'Pause' : 'Start'}
            </button>
            <button onClick={() => { setTime(0); setIsRunning(false); }}>
                Reset
            </button>
        </div>
    );
};

Pattern 3: Event Listeners

Window Resize Listener

const WindowSize: React.FC = () => {
    const [windowSize, setWindowSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight
    });
    
    useEffect(() => {
        const handleResize = () => {
            setWindowSize({
                width: window.innerWidth,
                height: window.innerHeight
            });
        };
        
        window.addEventListener('resize', handleResize);
        
        return () => {
            window.removeEventListener('resize', handleResize);
        };
    }, []);
    
    return (
        <div>
            <h2>Window Size</h2>
            <p>Width: {windowSize.width}px</p>
            <p>Height: {windowSize.height}px</p>
        </div>
    );
};

Keyboard Shortcuts

const KeyboardShortcuts: React.FC = () => {
    const [lastKey, setLastKey] = useState('');
    
    useEffect(() => {
        const handleKeyDown = (e: KeyboardEvent) => {
            // Ctrl+S to save
            if (e.ctrlKey && e.key === 's') {
                e.preventDefault();
                console.log('Save triggered');
                setLastKey('Ctrl+S');
            }
            
            // Escape to close
            if (e.key === 'Escape') {
                console.log('Close triggered');
                setLastKey('Escape');
            }
        };
        
        document.addEventListener('keydown', handleKeyDown);
        
        return () => {
            document.removeEventListener('keydown', handleKeyDown);
        };
    }, []);
    
    return (
        <div>
            <h2>Try Keyboard Shortcuts</h2>
            <p>Press Ctrl+S or Escape</p>
            <p>Last key: {lastKey}</p>
        </div>
    );
};

Pattern 4: Local Storage Sync

Persist State to localStorage

const PersistentCounter: React.FC = () => {
    const [count, setCount] = useState(() => {
        // Load from localStorage on mount
        const saved = localStorage.getItem('count');
        return saved ? parseInt(saved, 10) : 0;
    });
    
    // Save to localStorage whenever count changes
    useEffect(() => {
        localStorage.setItem('count', count.toString());
    }, [count]);
    
    return (
        <div>
            <h2>Count: {count}</h2>
            <button onClick={() => setCount(count + 1)}>Increment</button>
            <p>Refresh the page - count persists!</p>
        </div>
    );
};

Pattern 5: Focus Management

Auto-focus Input on Mount

import { useRef, useEffect } from 'react';

const AutoFocusInput: React.FC = () => {
    const inputRef = useRef<HTMLInputElement>(null);
    
    useEffect(() => {
        // Focus input when component mounts
        inputRef.current?.focus();
    }, []);
    
    return (
        <div>
            <label htmlFor="search">Search:</label>
            <input
                ref={inputRef}
                type="text"
                id="search"
                placeholder="Start typing..."
            />
        </div>
    );
};

Pattern 6: Click Outside Detection

Detect Clicks Outside Element

const DropdownMenu: React.FC = () => {
    const [isOpen, setIsOpen] = useState(false);
    const dropdownRef = useRef<HTMLDivElement>(null);
    
    useEffect(() => {
        if (!isOpen) return;
        
        const handleClickOutside = (event: MouseEvent) => {
            if (dropdownRef.current && 
                !dropdownRef.current.contains(event.target as Node)) {
                setIsOpen(false);
            }
        };
        
        document.addEventListener('mousedown', handleClickOutside);
        
        return () => {
            document.removeEventListener('mousedown', handleClickOutside);
        };
    }, [isOpen]);
    
    return (
        <div ref={dropdownRef}>
            <button onClick={() => setIsOpen(!isOpen)}>
                Toggle Menu
            </button>
            {isOpen && (
                <ul className="dropdown-menu">
                    <li>Option 1</li>
                    <li>Option 2</li>
                    <li>Option 3</li>
                </ul>
            )}
        </div>
    );
};

Pattern 7: Debouncing

Debounce Search Input

const SearchWithDebounce: React.FC = () => {
    const [searchTerm, setSearchTerm] = useState('');
    const [debouncedTerm, setDebouncedTerm] = useState('');
    const [results, setResults] = useState<string[]>([]);
    
    // Debounce the search term
    useEffect(() => {
        const timerId = setTimeout(() => {
            setDebouncedTerm(searchTerm);
        }, 500); // Wait 500ms after user stops typing
        
        return () => {
            clearTimeout(timerId);
        };
    }, [searchTerm]);
    
    // Fetch results when debounced term changes
    useEffect(() => {
        if (debouncedTerm) {
            console.log('Searching for:', debouncedTerm);
            // Simulate API call
            setResults([
                `Result 1 for "${debouncedTerm}"`,
                `Result 2 for "${debouncedTerm}"`,
                `Result 3 for "${debouncedTerm}"`
            ]);
        } else {
            setResults([]);
        }
    }, [debouncedTerm]);
    
    return (
        <div>
            <input
                type="search"
                value={searchTerm}
                onChange={(e) => setSearchTerm(e.target.value)}
                placeholder="Search..."
            />
            <ul>
                {results.map((result, i) => (
                    <li key={i}>{result}</li>
                ))}
            </ul>
        </div>
    );
};

๐Ÿ”„ Avoiding Infinite Loops

One of the most common mistakes with useEffect is creating infinite loops. Let's learn how to recognize and avoid them!

How Infinite Loops Happen

graph TD A["๐Ÿ”„ Component Renders"] --> B["โšก Effect Runs"] B --> C["๐Ÿ’พ Effect Updates State"] C --> D["๐Ÿ” State Change Triggers Re-render"] D --> A style A fill:#667eea,stroke:#5a67d8,color:#fff style B fill:#f44336,stroke:#d32f2f,color:#fff style C fill:#f44336,stroke:#d32f2f,color:#fff style D fill:#f44336,stroke:#d32f2f,color:#fff

๐ŸŽฎ Interactive: Infinite Loop Simulator

See what happens when you create an infinite loop (safely simulated!):

Render Count
0
Effect Count
0

Common Infinite Loop Scenarios

โš ๏ธ Scenario 1: No Dependency Array

// โŒ INFINITE LOOP!
const BadComponent: React.FC = () => {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        setCount(count + 1); // Updates state
    }); // No dependency array = runs after EVERY render
    
    return <div>{count}</div>;
};

// What happens:
// 1. Component renders
// 2. Effect runs, updates count
// 3. State change causes re-render
// 4. Effect runs again (no deps)
// 5. Back to step 2... forever! ๐Ÿ’ฅ

// โœ… FIX: Add empty dependency array
useEffect(() => {
    setCount(count + 1);
}, []); // Runs once on mount only

โš ๏ธ Scenario 2: Missing Dependencies (with state update)

// โŒ INFINITE LOOP!
const BadComponent: React.FC = () => {
    const [count, setCount] = useState(0);
    const [data, setData] = useState(null);
    
    useEffect(() => {
        fetch('/api/data')
            .then(res => res.json())
            .then(result => setData(result));
    }, [data]); // data is a dependency!
    
    return <div>{data}</div>;
};

// What happens:
// 1. Effect runs, fetches data
// 2. setData updates data
// 3. data changed, so effect runs again
// 4. Fetches again, updates data again
// 5. Infinite loop! ๐Ÿ’ฅ

// โœ… FIX: Remove data from dependencies
useEffect(() => {
    fetch('/api/data')
        .then(res => res.json())
        .then(result => setData(result));
}, []); // Fetch only once on mount

โš ๏ธ Scenario 3: Object/Array Dependencies

// โŒ INFINITE LOOP!
const BadComponent: React.FC = () => {
    const [user, setUser] = useState({ name: 'Alice', age: 30 });
    
    useEffect(() => {
        // Create new object
        setUser({ name: 'Alice', age: 30 });
    }, [user]); // user is new object every time!
    
    return <div>{user.name}</div>;
};

// What happens:
// 1. Effect runs
// 2. setUser creates NEW object
// 3. New object !== old object (different reference)
// 4. Effect sees change, runs again
// 5. Creates another new object... ๐Ÿ’ฅ

// โœ… FIX 1: Only depend on specific properties
useEffect(() => {
    setUser({ name: 'Alice', age: 30 });
}, [user.name, user.age]); // Primitives, not object

// โœ… FIX 2: Don't include in dependencies if not needed
useEffect(() => {
    // If user doesn't actually trigger the effect...
    console.log('Component mounted');
}, []); // Empty array

โš ๏ธ Scenario 4: Function Dependencies

// โŒ INFINITE LOOP!
const BadComponent: React.FC = () => {
    const [count, setCount] = useState(0);
    
    const incrementCount = () => {
        setCount(prev => prev + 1);
    };
    
    useEffect(() => {
        incrementCount();
    }, [incrementCount]); // New function every render!
    
    return <div>{count}</div>;
};

// โœ… FIX 1: Define function inside effect
useEffect(() => {
    const incrementCount = () => {
        setCount(prev => prev + 1);
    };
    incrementCount();
}, []); // No external dependencies

// โœ… FIX 2: Use useCallback for function
const incrementCount = useCallback(() => {
    setCount(prev => prev + 1);
}, []); // Memoized function

useEffect(() => {
    incrementCount();
}, [incrementCount]); // Same function reference

Debugging Infinite Loops

๐Ÿ’ก How to Find the Problem

  1. Check the Console: Look for "Maximum update depth exceeded" error
  2. Add Logging: Log when effect runs
    useEffect(() => {
        console.log('Effect running with deps:', { count, data });
        // Your effect code
    }, [count, data]);
  3. Check Dependencies: Are you updating something in the dependency array?
  4. Use React DevTools: Profiler can show re-render patterns
  5. Comment Out Code: Temporarily disable state updates to isolate the issue

Prevention Strategies

โœ… Best Practices to Avoid Loops

  • Always use dependency arrays: Never omit the second argument
  • Be careful with state updates: Don't update states that are dependencies
  • Use functional updates: setState(prev => ...) when depending on previous state
  • Memoize objects/arrays: Use useMemo/useCallback for complex dependencies
  • Depend on primitives: Use specific properties instead of whole objects
  • Understand reference equality: Objects/arrays/functions are compared by reference
  • Let ESLint help: Enable exhaustive-deps rule

Safe Pattern Examples

โœ… Correct Patterns

// Pattern 1: Fetch data once
useEffect(() => {
    fetch('/api/data')
        .then(res => res.json())
        .then(setData);
}, []); // Empty array = once on mount

// Pattern 2: Update based on prop
useEffect(() => {
    if (userId) {
        fetchUser(userId);
    }
}, [userId]); // Only when userId changes

// Pattern 3: Conditional state update
useEffect(() => {
    if (count > 10 && !hasShownAlert) {
        alert('Count is over 10!');
        setHasShownAlert(true); // Update different state
    }
}, [count, hasShownAlert]); // Won't loop if condition prevents it

// Pattern 4: Effect doesn't update its dependencies
const [input, setInput] = useState('');
const [debouncedInput, setDebouncedInput] = useState('');

useEffect(() => {
    const timer = setTimeout(() => {
        setDebouncedInput(input); // Updates different state
    }, 500);
    return () => clearTimeout(timer);
}, [input]); // input changes, but we don't update it

๐Ÿ‹๏ธ Hands-on Practice

Let's put everything together with comprehensive exercises!

๐Ÿ‹๏ธ Exercise 1: Real-Time Clock

Build a clock component that shows the current time and updates every second.

Requirements:

  • Display current time in HH:MM:SS format
  • Update every second
  • Clean up interval when component unmounts
  • Show date as well

Starter Code:

const Clock: React.FC = () => {
    const [time, setTime] = useState(new Date());
    
    // Your code here!
    
    const formatTime = (date: Date): string => {
        return date.toLocaleTimeString();
    };
    
    const formatDate = (date: Date): string => {
        return date.toLocaleDateString();
    };
    
    return (
        <div>
            <h2>{formatTime(time)}</h2>
            <p>{formatDate(time)}</p>
        </div>
    );
};
๐Ÿ’ก Hint

Use setInterval to update the time state every second. Don't forget to clean up!

useEffect(() => {
    const intervalId = setInterval(() => {
        // Update time
    }, 1000);
    
    return () => {
        // Cleanup
    };
}, []);
โœ… Solution
const Clock: React.FC = () => {
    const [time, setTime] = useState(new Date());
    
    useEffect(() => {
        const intervalId = setInterval(() => {
            setTime(new Date());
        }, 1000);
        
        return () => {
            clearInterval(intervalId);
        };
    }, []);
    
    const formatTime = (date: Date): string => {
        return date.toLocaleTimeString();
    };
    
    const formatDate = (date: Date): string => {
        return date.toLocaleDateString();
    };
    
    return (
        <div className="clock">
            <h2>{formatTime(time)}</h2>
            <p>{formatDate(time)}</p>
        </div>
    );
};

๐Ÿ‹๏ธ Exercise 2: Mouse Tracker

Create a component that tracks and displays mouse position.

Requirements:

  • Show X and Y coordinates of mouse
  • Update as mouse moves
  • Clean up event listener on unmount
  • Display if mouse is inside a specific area

Starter Code:

interface MousePosition {
    x: number;
    y: number;
}

const MouseTracker: React.FC = () => {
    const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
    
    // Your code here!
    
    return (
        <div style={{ height: '400px', border: '2px solid #667eea', padding: '20px' }}>
            <h2>Mouse Position</h2>
            <p>X: {position.x}</p>
            <p>Y: {position.y}</p>
        </div>
    );
};
โœ… Solution
const MouseTracker: React.FC = () => {
    const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
    
    useEffect(() => {
        const handleMouseMove = (e: MouseEvent) => {
            setPosition({
                x: e.clientX,
                y: e.clientY
            });
        };
        
        document.addEventListener('mousemove', handleMouseMove);
        
        return () => {
            document.removeEventListener('mousemove', handleMouseMove);
        };
    }, []);
    
    return (
        <div style={{ height: '400px', border: '2px solid #667eea', padding: '20px' }}>
            <h2>Mouse Position</h2>
            <p>X: {position.x}px</p>
            <p>Y: {position.y}px</p>
        </div>
    );
};

๐Ÿ‹๏ธ Exercise 3: Page Title Manager

Build a component that updates the document title based on different states.

Requirements:

  • Update title when notification count changes
  • Show "(N) Notifications" if there are unread items
  • Show app name when count is 0
  • Include page name in title

Starter Code:

const TitleManager: React.FC = () => {
    const [notifications, setNotifications] = useState(0);
    const [pageName, setPageName] = useState('Home');
    
    // Your code here!
    
    return (
        <div>
            <h2>Title Manager</h2>
            <p>Notifications: {notifications}</p>
            <button onClick={() => setNotifications(notifications + 1)}>
                Add Notification
            </button>
            <button onClick={() => setNotifications(0)}>
                Clear Notifications
            </button>
            <select value={pageName} onChange={(e) => setPageName(e.target.value)}>
                <option value="Home">Home</option>
                <option value="Profile">Profile</option>
                <option value="Settings">Settings</option>
            </select>
        </div>
    );
};
โœ… Solution
const TitleManager: React.FC = () => {
    const [notifications, setNotifications] = useState(0);
    const [pageName, setPageName] = useState('Home');
    
    useEffect(() => {
        let title = 'My App';
        
        if (notifications > 0) {
            title = `(${notifications}) ${title}`;
        }
        
        title = `${title} - ${pageName}`;
        
        document.title = title;
    }, [notifications, pageName]);
    
    return (
        <div>
            <h2>Title Manager</h2>
            <p>Notifications: {notifications}</p>
            <button onClick={() => setNotifications(notifications + 1)}>
                Add Notification
            </button>
            <button onClick={() => setNotifications(0)}>
                Clear Notifications
            </button>
            <select value={pageName} onChange={(e) => setPageName(e.target.value)}>
                <option value="Home">Home</option>
                <option value="Profile">Profile</option>
                <option value="Settings">Settings</option>
            </select>
        </div>
    );
};

๐Ÿ‹๏ธ Challenge Exercise: Pomodoro Timer

Build a complete Pomodoro timer with work and break sessions.

Requirements:

  • 25-minute work sessions
  • 5-minute break sessions
  • Auto-switch between work and break
  • Play sound when timer completes
  • Show notification when switching
  • Pause/resume functionality
  • Reset functionality

Bonus: Update document title with remaining time, save state to localStorage, add customizable durations!

โœจ Best Practices

โœ… Do's

  • Always use dependency arrays: Make effect behavior explicit
  • Clean up side effects: Remove listeners, clear timers, cancel requests
  • Keep effects focused: One effect per concern
  • Use early returns: Exit effect early when condition isn't met
  • Extract to custom hooks: Reuse effect logic across components
  • Type your effects: Use TypeScript for better safety
  • Use ESLint rule: Enable exhaustive-deps for automatic checks
  • Log during development: Add console.logs to understand execution
  • Consider effect timing: Use useLayoutEffect when DOM measurements needed

โŒ Don'ts

  • Don't omit dependency arrays: Leads to unpredictable behavior
  • Don't ignore ESLint warnings: They usually indicate real problems
  • Don't make effect function async: Define async function inside instead
  • Don't update state dependencies: Causes infinite loops
  • Don't forget cleanup: Memory leaks are hard to debug
  • Don't put too much in one effect: Split into multiple focused effects
  • Don't depend on objects/arrays directly: Use specific properties
  • Don't use effects for derived state: Calculate during render instead

Effect Organization

โœ… Good: Separate Concerns

const MyComponent: React.FC = () => {
    // Effect 1: Document title
    useEffect(() => {
        document.title = `Page ${page}`;
    }, [page]);
    
    // Effect 2: Data fetching
    useEffect(() => {
        fetchData();
    }, [userId]);
    
    // Effect 3: Event listener
    useEffect(() => {
        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
    }, []);
    
    // Each effect has single responsibility
};

โŒ Bad: Everything in One Effect

const MyComponent: React.FC = () => {
    useEffect(() => {
        // Too much in one effect!
        document.title = `Page ${page}`;
        fetchData();
        window.addEventListener('resize', handleResize);
        
        return () => {
            window.removeEventListener('resize', handleResize);
        };
    }, [page, userId]); // Runs for both changes
};

When NOT to Use useEffect

๐Ÿ’ก Consider Alternatives

// โŒ DON'T: Use effect for derived state
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);

useEffect(() => {
    setTotal(items.reduce((sum, item) => sum + item.price, 0));
}, [items]);

// โœ… DO: Calculate during render
const [items, setItems] = useState([]);
const total = items.reduce((sum, item) => sum + item.price, 0);

// โŒ DON'T: Use effect to transform props
useEffect(() => {
    setUpperName(name.toUpperCase());
}, [name]);

// โœ… DO: Transform during render
const upperName = name.toUpperCase();

// โŒ DON'T: Use effect for event handlers
useEffect(() => {
    if (shouldSubmit) {
        submitForm();
    }
}, [shouldSubmit]);

// โœ… DO: Call directly in event handler
const handleClick = () => {
    submitForm();
};

Performance Considerations

Optimize Effect Execution

// โŒ Runs on every render
useEffect(() => {
    expensiveOperation();
});

// โœ… Runs only when needed
useEffect(() => {
    expensiveOperation();
}, [dependency]);

// โœ… Early return for unnecessary work
useEffect(() => {
    if (!shouldRun) return;
    expensiveOperation();
}, [shouldRun, dependency]);

๐Ÿ“š Summary

What You Learned

Congratulations! You've mastered the useEffect hookโ€”one of React's most powerful and essential features:

  • โœ… Understanding side effects and why they need special handling
  • โœ… Using useEffect to run code after renders
  • โœ… Mastering dependency arrays for controlling when effects run
  • โœ… Implementing cleanup functions to prevent memory leaks
  • โœ… Understanding effect execution timing and lifecycle
  • โœ… Common patterns: timers, event listeners, document updates
  • โœ… Avoiding infinite loops and other common pitfalls
  • โœ… Writing clean, maintainable effects with TypeScript

๐ŸŽฏ Key Takeaways

  • Pure render functions: Side effects go in useEffect, not render
  • Dependency arrays matter: They control when effects run
  • Always clean up: Prevent memory leaks and stale updates
  • One effect per concern: Keep effects focused and maintainable
  • Trust ESLint: The exhaustive-deps rule catches real problems

useEffect Quick Reference

Pattern Code When It Runs
Every Render useEffect(() => {}) After every render
Mount Only useEffect(() => {}, []) Once when component mounts
On Change useEffect(() => {}, [dep]) When dep changes
With Cleanup useEffect(() => { return () => {} }, []) Cleanup on unmount/re-run

Common Use Cases Cheatsheet

// Document title
useEffect(() => {
    document.title = title;
}, [title]);

// Timer
useEffect(() => {
    const id = setInterval(() => {}, 1000);
    return () => clearInterval(id);
}, []);

// Event listener
useEffect(() => {
    window.addEventListener('event', handler);
    return () => window.removeEventListener('event', handler);
}, []);

// Focus input
useEffect(() => {
    inputRef.current?.focus();
}, []);

// localStorage sync
useEffect(() => {
    localStorage.setItem('key', value);
}, [value]);

๐Ÿš€ What's Next?

In the next lesson, we'll learn about Data Fetching Basics:

  • Using fetch API with useEffect
  • Handling loading and error states
  • Typing API responses with TypeScript
  • Async/await patterns in effects
  • Canceling requests with AbortController
  • Building real-world data fetching components

You're building real React applications now! ๐Ÿ’ช