Skip
Arish's avatar

9. useEffect Hook


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!