โก 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
๐ฎ 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
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
๐ฎ Interactive: Dependency Array Simulator
Simulate component renders and see when your effect runs based on different dependency configurations:
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
๐ฎ Interactive: Effect & Cleanup Lifecycle
Watch how effects and cleanup functions run during the component lifecycle:
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
๐ฎ Interactive: Effect Execution Timeline
Watch the exact order of execution during a React render cycle:
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
๐ฎ Interactive: Infinite Loop Simulator
See what happens when you create an infinite loop (safely simulated!):
Render Count
Effect Count
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
- Check the Console: Look for "Maximum update depth exceeded" error
- Add Logging: Log when effect runs
useEffect(() => { console.log('Effect running with deps:', { count, data }); // Your effect code }, [count, data]); - Check Dependencies: Are you updating something in the dependency array?
- Use React DevTools: Profiler can show re-render patterns
- 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! ๐ช