useEffect Hook
The useEffect hook lets you perform side effects in function components. Side effects include data fetching, subscriptions, timers, and manual DOM manipulation.
Basic Syntax
jsx
1import { useEffect } from 'react'
2
3function Component() {
4 useEffect(() => {
5 // Side effect code here
6 console.log('Effect ran!')
7
8 // Optional cleanup function
9 return () => {
10 console.log('Cleanup!')
11 }
12 }, [dependencies])
13}Effect Dependencies
No Dependencies Array - Runs on Every Render
jsx
1function Component() {
2 useEffect(() => {
3 console.log('I run on every render')
4 })
5 // ⚠️ Rarely what you want - can cause infinite loops
6}Empty Dependencies Array - Runs Once on Mount
jsx
1function Component() {
2 useEffect(() => {
3 console.log('I run once on mount')
4
5 return () => {
6 console.log('I run once on unmount')
7 }
8 }, [])
9}With Dependencies - Runs When Dependencies Change
jsx
1function Component({ userId }) {
2 const [user, setUser] = useState(null)
3
4 useEffect(() => {
5 console.log('I run when userId changes')
6 fetchUser(userId).then(setUser)
7 }, [userId])
8}Data Fetching
jsx
1function UserProfile({ userId }) {
2 const [user, setUser] = useState(null)
3 const [loading, setLoading] = useState(true)
4 const [error, setError] = useState(null)
5
6 useEffect(() => {
7 const fetchUser = async () => {
8 try {
9 setLoading(true)
10 setError(null)
11 const response = await fetch(`/api/users/${userId}`)
12
13 if (!response.ok) {
14 throw new Error('Failed to fetch user')
15 }
16
17 const data = await response.json()
18 setUser(data)
19 } catch (err) {
20 setError(err.message)
21 } finally {
22 setLoading(false)
23 }
24 }
25
26 fetchUser()
27 }, [userId])
28
29 if (loading) return <p>Loading...</p>
30 if (error) return <p>Error: {error}</p>
31 if (!user) return null
32
33 return <div>{user.name}</div>
34}Cleanup Functions
Cleanup prevents memory leaks and stale updates:
Subscriptions
jsx
1function ChatRoom({ roomId }) {
2 useEffect(() => {
3 const connection = createConnection(roomId)
4 connection.connect()
5
6 // Cleanup: disconnect when roomId changes or unmount
7 return () => {
8 connection.disconnect()
9 }
10 }, [roomId])
11}Timers
jsx
1function Timer() {
2 const [count, setCount] = useState(0)
3
4 useEffect(() => {
5 const intervalId = setInterval(() => {
6 setCount(c => c + 1)
7 }, 1000)
8
9 // Cleanup: clear interval on unmount
10 return () => clearInterval(intervalId)
11 }, [])
12
13 return <p>Count: {count}</p>
14}Event Listeners
jsx
1function WindowSize() {
2 const [size, setSize] = useState({
3 width: window.innerWidth,
4 height: window.innerHeight
5 })
6
7 useEffect(() => {
8 const handleResize = () => {
9 setSize({
10 width: window.innerWidth,
11 height: window.innerHeight
12 })
13 }
14
15 window.addEventListener('resize', handleResize)
16
17 // Cleanup: remove listener on unmount
18 return () => window.removeEventListener('resize', handleResize)
19 }, [])
20
21 return <p>{size.width} x {size.height}</p>
22}Abort Fetch Requests
Cancel requests when component unmounts or dependencies change:
jsx
1function SearchResults({ query }) {
2 const [results, setResults] = useState([])
3
4 useEffect(() => {
5 const controller = new AbortController()
6
7 const fetchResults = async () => {
8 try {
9 const response = await fetch(`/api/search?q=${query}`, {
10 signal: controller.signal
11 })
12 const data = await response.json()
13 setResults(data)
14 } catch (err) {
15 if (err.name !== 'AbortError') {
16 console.error(err)
17 }
18 }
19 }
20
21 fetchResults()
22
23 // Cleanup: abort request
24 return () => controller.abort()
25 }, [query])
26
27 return <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
28}Multiple Effects
Separate effects by concern:
jsx
1function UserDashboard({ userId }) {
2 const [user, setUser] = useState(null)
3 const [posts, setPosts] = useState([])
4
5 // Effect for user data
6 useEffect(() => {
7 fetchUser(userId).then(setUser)
8 }, [userId])
9
10 // Effect for posts data
11 useEffect(() => {
12 fetchPosts(userId).then(setPosts)
13 }, [userId])
14
15 // Effect for document title
16 useEffect(() => {
17 if (user) {
18 document.title = `${user.name}'s Dashboard`
19 }
20
21 return () => {
22 document.title = 'My App'
23 }
24 }, [user])
25}Common Patterns
Debouncing
jsx
1function SearchInput() {
2 const [query, setQuery] = useState('')
3 const [debouncedQuery, setDebouncedQuery] = useState('')
4
5 useEffect(() => {
6 const timeoutId = setTimeout(() => {
7 setDebouncedQuery(query)
8 }, 500)
9
10 return () => clearTimeout(timeoutId)
11 }, [query])
12
13 useEffect(() => {
14 if (debouncedQuery) {
15 // Perform search
16 console.log('Searching for:', debouncedQuery)
17 }
18 }, [debouncedQuery])
19
20 return (
21 <input
22 value={query}
23 onChange={(e) => setQuery(e.target.value)}
24 />
25 )
26}Local Storage Sync
jsx
1function usePersistentState(key, initialValue) {
2 const [value, setValue] = useState(() => {
3 const saved = localStorage.getItem(key)
4 return saved ? JSON.parse(saved) : initialValue
5 })
6
7 useEffect(() => {
8 localStorage.setItem(key, JSON.stringify(value))
9 }, [key, value])
10
11 return [value, setValue]
12}
13
14// Usage
15function App() {
16 const [theme, setTheme] = usePersistentState('theme', 'light')
17}Focus Management
jsx
1function AutoFocusInput() {
2 const inputRef = useRef(null)
3
4 useEffect(() => {
5 inputRef.current?.focus()
6 }, [])
7
8 return <input ref={inputRef} />
9}Effect Execution Order
jsx
1function Component() {
2 console.log('1. Render')
3
4 useEffect(() => {
5 console.log('3. Effect runs')
6
7 return () => {
8 console.log('2. Cleanup from previous effect')
9 }
10 })
11
12 // Order: Render → Previous Cleanup → Effect
13}Common Mistakes
Missing Dependencies
jsx
1// ❌ Bug: effect uses count but doesn't list it
2function Counter() {
3 const [count, setCount] = useState(0)
4
5 useEffect(() => {
6 const id = setInterval(() => {
7 setCount(count + 1) // Always uses initial count (0)
8 }, 1000)
9 return () => clearInterval(id)
10 }, []) // Missing count
11}
12
13// ✅ Fix with functional update
14function Counter() {
15 const [count, setCount] = useState(0)
16
17 useEffect(() => {
18 const id = setInterval(() => {
19 setCount(c => c + 1) // Uses latest count
20 }, 1000)
21 return () => clearInterval(id)
22 }, [])
23}Infinite Loops
jsx
1// ❌ Infinite loop: effect creates new object every render
2function Component() {
3 const [data, setData] = useState(null)
4 const options = { method: 'GET' } // New object every render
5
6 useEffect(() => {
7 fetch('/api/data', options).then(res => res.json()).then(setData)
8 }, [options]) // options changes every render!
9}
10
11// ✅ Fix: memoize or move inside effect
12function Component() {
13 const [data, setData] = useState(null)
14
15 useEffect(() => {
16 const options = { method: 'GET' }
17 fetch('/api/data', options).then(res => res.json()).then(setData)
18 }, []) // No external dependency
19}useEffect is essential for handling side effects in React!
