So far, your React components have been pure functions - given the same props and state, they return the same JSX. But real applications need to do more: fetch data from APIs, update the document title, set up timers, connect to WebSockets. These are called side effects, and React provides the useEffect hook to handle them. Let's learn how to synchronize your components with the world outside React!
What Are Side Effects?
A side effect is any operation that affects something outside the component's render:
Examples of Side Effects:
- Data fetching - API calls, database queries
- Subscriptions - WebSocket connections, event listeners
- Timers - setTimeout, setInterval
- DOM manipulation - Changing document.title, focus
- Logging - Analytics, error tracking
- Local storage - Reading/writing localStorage
Pure Function vs Side Effect:
Pure (render logic):const doubled = count * 2
Takes input, returns output, no external changes
Side Effect:fetch('/api/data')
Interacts with systems outside the component
The Problem: Where to Put Side Effects?
You can't put side effects directly in the component body:
function Component() {
const [data, setData] = React.useState(null)
// ❌ BAD - This runs on EVERY render!
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data)) // This causes a re-render
// Which runs fetch again... infinite loop!
return <div>{data ? data.name : 'Loading...'}</div>
}
// Problems:
// 1. Fetch runs on every render
// 2. setState causes re-render
// 3. Re-render triggers fetch again
// 4. Infinite loop!You also can't put them in event handlers if they need to run automatically:
function Component() {
const [data, setData] = React.useState(null)
const fetchData = () => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data))
}
// ❌ Data never loads - user must click button
return (
<div>
<button onClick={fetchData}>Load Data</button>
<div>{data ? data.name : 'Click to load'}</div>
</div>
)
}
// Problem: What if you want data to load automatically?The Solution: useEffect
useEffect tells React: "Run this code after rendering":
function Component() {
const [data, setData] = React.useState(null)
// ✅ GOOD - useEffect runs AFTER render
React.useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data))
}, []) // Empty array = run once after first render
return <div>{data ? data.name : 'Loading...'}</div>
}
// Flow:
// 1. Component renders (shows "Loading...")
// 2. useEffect runs fetch
// 3. Data arrives, setState called
// 4. Component re-renders (shows data.name)
// 5. useEffect doesn't run again (empty dependency array)useEffect Syntax
React.useEffect(() => {
// Effect code runs after render
console.log('Effect ran!')
// Optional cleanup function
return () => {
console.log('Cleanup ran!')
}
}, [dependencies]) // Dependency array
// Three parts:
// 1. Effect function - runs after render
// 2. Cleanup function (optional) - runs before effect re-runs or unmount
// 3. Dependency array - controls when effect runsBasic useEffect Examples
Example 1: Document Title
function Counter() {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
// Update document title after every render
document.title = `Count: ${count}`
}) // No dependency array = runs after every render
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
// Every time count changes and component re-renders,
// useEffect updates the document titleExample 2: Run Once on Mount
function Welcome() {
React.useEffect(() => {
console.log('Component mounted!')
// This only runs once when component first renders
}, []) // Empty array = run once on mount
return <h1>Welcome!</h1>
}
// Flow:
// 1. Component renders
// 2. Effect runs once
// 3. Component can re-render for other reasons
// 4. Effect doesn't run againExample 3: Run When Dependency Changes
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null)
React.useEffect(() => {
console.log('Fetching user:', userId)
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data))
}, [userId]) // Run when userId changes
return (
<div>
{user ? <p>Name: {user.name}</p> : <p>Loading...</p>}
</div>
)
}
// Effect runs when:
// 1. Component first mounts (initial render)
// 2. userId prop changes
// If userId stays the same, effect doesn't re-runExample 4: Local Storage
function ThemeSwitcher() {
const [theme, setTheme] = React.useState(() => {
// Load from localStorage on mount
return localStorage.getItem('theme') || 'light'
})
React.useEffect(() => {
// Save to localStorage whenever theme changes
localStorage.setItem('theme', theme)
console.log('Saved theme:', theme)
}, [theme])
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
)
}
// Automatically syncs theme with localStorageUnderstanding the Dependency Array
The dependency array controls when your effect runs:
No Dependency Array - Runs After Every Render
React.useEffect(() => {
console.log('Effect ran!')
}) // No array = runs after EVERY render
// Runs when:
// - Component mounts
// - Any state changes
// - Any prop changes
// - Parent re-renders
// Use when: You need to sync with something on every render
// Example: Updating document.title with current stateEmpty Dependency Array - Runs Once
React.useEffect(() => {
console.log('Effect ran once!')
}, []) // Empty array = runs ONCE on mount
// Runs when:
// - Component mounts (first render)
// Never runs again!
// Use when: Setup that should happen once
// Examples: Fetching initial data, setting up subscriptionsWith Dependencies - Runs When Dependencies Change
React.useEffect(() => {
console.log('Effect ran!')
}, [count, userId]) // Array with values = runs when they change
// Runs when:
// - Component mounts (first render)
// - count changes
// - userId changes
// Use when: Effect depends on specific values
// Examples: Fetching data based on ID, updating based on state📋 Dependency Array Rules
Include in the dependency array:
✅ Props used in the effect
✅ State used in the effect
✅ Functions defined in the component
Don't include:
❌ setState functions (they're stable)
❌ Values from outside the component
❌ Constants that never change
When Does useEffect Run?
Understanding the execution order is crucial:
function Component() {
console.log('1. Component rendering')
const [count, setCount] = React.useState(0)
console.log('2. Component function running')
React.useEffect(() => {
console.log('4. Effect running (after render)')
})
console.log('3. About to return JSX')
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}
// Console output:
// "1. Component rendering"
// "2. Component function running"
// "3. About to return JSX"
// (Browser paints the screen)
// "4. Effect running (after render)"
// Key point: Effect runs AFTER the browser paints
// This prevents blocking the UIComplete Render Cycle
- React calls your component function
- Component returns JSX
- React updates the DOM
- Browser paints the screen (user sees changes)
- useEffect runs
Why Run After Paint?
Effects run after the browser paints so they don't block the UI update. This keeps your app responsive. If an effect takes time (like a network request), the user still sees the UI update immediately.
Interactive Demo
💡 Tip: Edit the code above and click "Run" to see your changes
Common useEffect Mistakes
1. Missing Dependencies
function Component() {
const [count, setCount] = React.useState(0)
// ❌ Wrong - count is used but not in dependencies
React.useEffect(() => {
console.log('Count is:', count)
}, []) // Should include count!
// ✅ Correct
React.useEffect(() => {
console.log('Count is:', count)
}, [count])
}2. Infinite Loop
function Component() {
const [data, setData] = React.useState([])
// ❌ Infinite loop!
React.useEffect(() => {
setData([...data, 'new item']) // Updates data
}, [data]) // Which triggers effect again!
// ✅ Correct - use functional update
React.useEffect(() => {
setData(prev => [...prev, 'new item'])
}, []) // Run once
}3. Effect in Wrong Place
// ❌ Wrong - effect should be inside component
React.useEffect(() => {
console.log('Effect')
}, [])
function Component() {
return <div>Component</div>
}
// ✅ Correct - hooks must be called inside component
function Component() {
React.useEffect(() => {
console.log('Effect')
}, [])
return <div>Component</div>
}4. Conditional useEffect
function Component({ isActive }) {
// ❌ Wrong - hooks can't be conditional
if (isActive) {
React.useEffect(() => {
console.log('Active')
}, [])
}
// ✅ Correct - condition inside effect
React.useEffect(() => {
if (isActive) {
console.log('Active')
}
}, [isActive])
}Key Takeaways
- Side effects are operations outside the component (API calls, timers, etc.)
useEffectruns code after rendering- Effects run after the browser paints (non-blocking)
- No dependency array = runs after every render
- Empty array [] = runs once on mount
- [dependencies] = runs when dependencies change
- Include all values used in effect in dependency array
- Effects can return a cleanup function (covered in next lesson)
- Don't put effects directly in component body (infinite loops!)
- Hooks must be called at the top level (not conditionally)
- Common uses: data fetching, subscriptions, timers, DOM updates
- useEffect synchronizes React with external systems
What's Next?
You now understand the basics of useEffect and how to handle side effects in React! You know when effects run and how to control them with dependency arrays.
In the next lesson, you'll learn about Data Fetching:
- Fetching data from APIs with useEffect
- Handling loading and error states
- Async/await patterns in useEffect
- Preventing race conditions
- Building custom data fetching hooks
Get ready to connect your React apps to real data sources! 🌐