L1: Introduction to useEffect
Learn how to handle side effects in React components using the useEffect hook
What You'll Learn
In this lesson, you'll discover how React handles side effects - operations that reach outside your component to interact with the browser, APIs, or external systems. You'll learn:
- What side effects are and why they matter
- How to use the
useEffecthook - Understanding the dependency array
- Common patterns and best practices
- When to use useEffect vs when not to
Understanding Side Effects
What is a Side Effect?
In React, a side effect is any operation that affects something outside the scope of the current component being rendered. Think of it as reaching beyond your component's borders.
Examples of side effects:
- 🌐 Fetching data from an API
- 📝 Updating the document title
- ⏰ Setting up timers or intervals
- 🔔 Subscribing to external data sources
- 💾 Reading/writing to localStorage
- 📊 Logging analytics events
- 🎨 Manually manipulating the DOM
Key Concept: Side effects happen outside the normal React render flow. They're operations that can't be done during rendering.
Why Do We Need useEffect?
React components should be pure functions during rendering - they take props and state, and return JSX. But real applications need to do more:
function Component() {
// This runs during EVERY render!
document.title = "My App"; // Side effect!
fetch("/api/listings"); // Side effect!
return <div>Component</div>;
}This causes problems:
- 🔄 Side effects run on every render (too often!)
- 🐛 No way to clean up (memory leaks)
- ⚡ Can't control when they run
- 🔁 Can cause infinite loops
useEffect solves this:
import { useEffect } from 'react';
function Component() {
useEffect(() => {
document.title = "My App";
}, []); // Runs only once!
return <div>Component</div>;
}The useEffect Hook
Basic Syntax
import { useEffect } from 'react';
function Component() {
useEffect(() => {
// Your side effect code here
console.log('Effect ran!');
}, [dependencies]);
return <div>Component</div>;
}The three parts:
- Effect function - The code you want to run
- Dependencies array - Controls when the effect runs
- Cleanup function (optional) - Runs before the effect re-runs or component unmounts
When Does useEffect Run?
The timing depends on the dependency array:
useEffect(() => {
console.log('Component mounted!');
}, []); // Empty array = run onceWhen: Once, after the first render
Use case: Initial data fetching, subscriptions, one-time setup
useEffect(() => {
console.log(`Count changed to ${count}`);
}, [count]); // Runs when count changesWhen: After first render, and whenever dependencies change
Use case: Syncing with external systems, responding to state changes
useEffect(() => {
console.log('Component rendered!');
}); // No array = run every renderWhen: After every render (usually not what you want!)
Use case: Rare - usually indicates a code smell
Important: useEffect runs after the component renders and the browser paints the screen. This prevents blocking the UI.
Common Patterns
Pattern 1: Document Title
Update the browser tab title based on component state:
import { useState, useEffect } from 'react';
function HomePage() {
const [listings, setListings] = useState([]);
useEffect(() => {
document.title = `StaySense - ${listings.length} listings`;
}, [listings]); // Updates when listings change
return (
<div>
<h1>Available Listings: {listings.length}</h1>
{/* ... */}
</div>
);
}Why it works:
- Effect runs when
listingschanges - Document title stays in sync with state
- Clean and declarative
Pattern 2: Console Logging (Debugging)
Track when your component renders and why:
import { useEffect } from 'react';
function PropertyCard({ listing }) {
useEffect(() => {
console.log('PropertyCard mounted:', listing.id);
return () => {
console.log('PropertyCard unmounting:', listing.id);
};
}, [listing.id]);
useEffect(() => {
console.log('Listing data changed:', listing);
}, [listing]);
return <div>{listing.title}</div>;
}What happens:
- First effect logs when component mounts/unmounts
- Second effect logs when listing data changes
- Cleanup function runs before unmount
Pattern 3: Timer/Interval
Set up timers that automatically clean up:
import { useState, useEffect } from 'react';
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date());
}, 1000);
// Cleanup: Clear interval when component unmounts
return () => clearInterval(interval);
}, []); // Empty array = set up once
return <div>{time.toLocaleTimeString()}</div>;
}Key points:
setIntervalis a side effect (it runs a background timer)- Cleanup function prevents memory leaks
- Empty dependency array means timer is set up once
Pattern 4: Event Listeners
Add and remove event listeners properly:
import { useState, useEffect } from 'react';
function ResponsiveComponent() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
};
// Add listener
window.addEventListener('resize', handleResize);
// Cleanup: Remove listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Set up once
return <div>Window width: {width}px</div>;
}Rules of useEffect
Comparison: useEffect vs Class Lifecycle
If you're coming from class components, here's how useEffect relates:
class Component extends React.Component {
componentDidMount() {
fetchData();
}
}function Component() {
useEffect(() => {
fetchData();
}, []); // Empty array = mount only
}class Component extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
fetchUser(this.props.userId);
}
}
}function Component({ userId }) {
useEffect(() => {
fetchUser(userId);
}, [userId]); // Runs when userId changes
}class Component extends React.Component {
componentWillUnmount() {
subscription.unsubscribe();
}
}function Component() {
useEffect(() => {
return () => {
subscription.unsubscribe();
};
}, []);
}class Component extends React.Component {
componentDidMount() {
this.subscribe();
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.unsubscribe();
this.subscribe();
}
}
componentWillUnmount() {
this.unsubscribe();
}
subscribe() { /* ... */ }
unsubscribe() { /* ... */ }
}function Component({ id }) {
useEffect(() => {
subscribe(id);
return () => unsubscribe(id);
}, [id]); // Handles all three lifecycle methods!
}Much simpler! One useEffect handles mount, update, and unmount.
When NOT to Use useEffect
Some things don't need useEffect:
❌ Don't: Calculate Derived State
function Component({ items }) {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(items.length); // Unnecessary effect!
}, [items]);
return <div>{count} items</div>;
}function Component({ items }) {
const count = items.length; // Just calculate it!
return <div>{count} items</div>;
}❌ Don't: Handle User Events
function Component() {
const [clicked, setClicked] = useState(false);
useEffect(() => {
if (clicked) {
alert('Clicked!');
setClicked(false);
}
}, [clicked]);
return <button onClick={() => setClicked(true)}>Click</button>;
}function Component() {
const handleClick = () => {
alert('Clicked!');
};
return <button onClick={handleClick}>Click</button>;
}Use event handlers for user interactions, not useEffect!
❌ Don't: Initialize State
function Component({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
setUser({ id: userId, name: 'Unknown' });
}, []);
return <div>{user?.name}</div>;
}function Component({ userId }) {
const [user, setUser] = useState({
id: userId,
name: 'Unknown'
});
return <div>{user.name}</div>;
}Practice Exercise
Let's practice what you've learned! Try to predict what happens:
Exercise 1: Predict the Output
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Effect ran, count:', count);
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}Questions:
- When does the console.log run?
- How many times if you click the button 3 times?
- What if the dependency array was
[]?
Exercise 2: Fix the Bug
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
setInterval(() => {
setSeconds(seconds + 1);
}, 1000);
}, []);
return <div>{seconds} seconds</div>;
}Problems:
- Missing cleanup - interval never stops
- Stale closure -
secondsis always 0 - New interval created on every render
Key Takeaways
You've learned the fundamentals!
Understanding Side Effects:
- Side effects reach outside your component
- Common examples: API calls, timers, subscriptions
- useEffect lets you control when side effects run
useEffect Syntax:
- Effect function contains your side effect code
- Dependency array controls when it runs
- Cleanup function runs before re-running or unmounting
Best Practices:
- Always include dependencies
- Clean up side effects (timers, listeners, subscriptions)
- Don't use useEffect for derived state or event handlers
- Use ESLint plugin to catch mistakes
Next Up: In the next lesson, we'll set up a mock API and use useEffect to fetch real data for our StaySense listings!
What's Next?
Now that you understand the basics of useEffect, you're ready to use it for real-world scenarios. In the next lesson, you'll:
- 🛠️ Create a mock API for listing data
- 📡 Use useEffect to fetch data on component mount
- ⚡ Learn async/await patterns with useEffect
- 🔄 Update your HomePage to load data dynamically
Ready to fetch some data? Let's go! 🚀