React Hooks — useEffect
What is useEffect?
useEffect lets you perform side effects in function components — things that happen outside the normal render cycle: fetching data, subscribing to events, updating the document title, setting up timers, and more.
It runs after the component renders. The dependency array controls when it re-runs.
useEffect Dependency Array
| Syntax | When it runs |
|---|---|
useEffect(() => {...}) | After every render |
useEffect(() => {...}, []) | Only once — on mount (componentDidMount) |
useEffect(() => {...}, [a, b]) | When a or b changes |
useEffect(() => { return () => {...} }, []) | Cleanup on unmount |
import { useState, useEffect } from 'react'
function UserList() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// Fetch data on mount — [] means run once
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true)
const res = await fetch('https://jsonplaceholder.typicode.com/users')
if (!res.ok) throw new Error('Failed to fetch')
const data = await res.json()
setUsers(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchUsers()
}, []) // empty array = run once on mount
if (loading) return <p>Loading...</p>
if (error) return <p>Error: {error}</p>
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
)
}
// Fetch when dependency changes
function UserDetail({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
if (!userId) return
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data))
}, [userId]) // re-runs whenever userId changes
return user ? <div>{user.name}</div> : <p>Select a user</p>
}
import { useState, useEffect } from 'react'
// Cleanup — prevent memory leaks
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1)
}, 1000)
// Cleanup function — runs on unmount or before next effect
return () => clearInterval(interval)
}, [])
return <p>Timer: {seconds}s</p>
}
// Event listener cleanup
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight })
}
window.addEventListener('resize', handleResize)
// Cleanup — remove listener when component unmounts
return () => window.removeEventListener('resize', handleResize)
}, [])
return <p>{size.width} × {size.height}</p>
}
// AbortController — cancel fetch on unmount
function SearchResults({ query }) {
const [results, setResults] = useState([])
useEffect(() => {
const controller = new AbortController()
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(data => setResults(data))
.catch(err => {
if (err.name !== 'AbortError') console.error(err)
})
return () => controller.abort() // cancel on unmount or query change
}, [query])
return <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
}
import { useState, useEffect } from 'react'
function AllEffectPatterns() {
const [count, setCount] = useState(0)
const [name, setName] = useState('')
// 1. Run after EVERY render (no dependency array)
useEffect(() => {
console.log('Rendered!')
})
// 2. Run ONCE on mount
useEffect(() => {
document.title = 'My App'
console.log('Component mounted')
}, [])
// 3. Run when count changes
useEffect(() => {
document.title = `Count: ${count}`
}, [count])
// 4. Run when name changes, with cleanup
useEffect(() => {
const timeout = setTimeout(() => {
console.log('Searching for:', name)
}, 500) // debounce
return () => clearTimeout(timeout)
}, [name])
// 5. Multiple effects — separate concerns
useEffect(() => {
// Analytics tracking
console.log('Page view tracked')
}, [])
useEffect(() => {
// Keyboard shortcut
const handler = (e) => {
if (e.key === 'Escape') console.log('Escape pressed')
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [])
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
)
}
Key Takeaways
-
useStatereturns [value, setter] — always use the setter, never mutate state directly. - State updates are asynchronous — the new value is not available immediately after calling the setter.
-
useEffectruns after every render by default. Pass a dependency array to control when it runs. -
Empty dependency array
[]means the effect runs only once after the first render (like componentDidMount). - Return a cleanup function from useEffect to prevent memory leaks (clear timers, cancel subscriptions).
- Never call hooks inside loops, conditions, or nested functions — always at the top level of a component.
Common Mistakes to Avoid
WRONG
state.items.push(newItem); setState(state)
RIGHT
setState(prev => ({...prev, items:[...prev.items, newItem]}))
Never mutate state directly. Always create a new object/array to trigger a re-render.
WRONG
useEffect(()=>{ fetchData(); })
RIGHT
useEffect(()=>{ fetchData(); }, [])
Without a dependency array, the effect runs on every render — causing infinite loops with state updates inside.
WRONG
const [count, setCount] = useState(); setCount(count+1)
RIGHT
setCount(prev => prev + 1)
Use the functional updater form when new state depends on old state — avoids stale closure bugs.
Frequently Asked Questions
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.