useEffect & Side Effects

Easy

useEffect synchronizes a component with an external system — network requests, DOM manipulation, timers, or subscriptions. It runs after the browser paints, and its cleanup function runs before the next effect or on unmount. The dependency array controls when the effect re-runs.

Interactive Visualization

Key Points

  • Runs after render and browser paint (asynchronous)
  • Dependency array controls when the effect re-runs
  • Empty dependency array [] means run only on mount
  • No dependency array means run after every render
  • Cleanup function runs before re-running the effect and on unmount
  • Each render has its own effect with captured values from that render
  • StrictMode in development runs effects twice to detect missing cleanups

Code Examples

Data Fetching with Cleanup

import { useState, useEffect } from 'react'

interface User {
  id: string
  name: string
}

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const controller = new AbortController()
    setLoading(true)

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then((res) => res.json() as Promise<User>)
      .then((data) => {
        setUser(data)
        setLoading(false)
      })
      .catch((err: Error) => {
        if (err.name !== 'AbortError') {
          setLoading(false)
        }
      })

    return () => controller.abort()
  }, [userId])

  if (loading) return <div>Loading...</div>
  return <div>{user?.name}</div>
}

AbortController in the cleanup function cancels the previous fetch when userId changes, preventing race conditions

Event Listener Subscription

import { useState, useEffect } from 'react'

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)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return <p>{size.width} x {size.height}</p>
}

The empty dependency array ensures the listener is added once on mount and removed on unmount via the cleanup return

Common Mistakes

  • Omitting dependencies that are used inside the effect, causing stale closures
  • Adding object or function dependencies that change every render, causing infinite loops
  • Forgetting the cleanup function for subscriptions, timers, or event listeners

Interview Tips

  • Explain the mental model: effects synchronize with external systems, not lifecycle events
  • Know how to prevent race conditions with AbortController or an ignore flag
  • Discuss why useEffect runs after paint and when useLayoutEffect is needed instead

Related Concepts