Devlopr

Understanding useEffect in React with Examples

Siddharth Patel
Siddharth Patel
Full-stack developer
Published on July 28, 2025

When you start working with React, one of the most important hooks you’ll encounter is useEffect. At first, it might seem confusing. Why does it behave differently than what you expect? Why does it keep re-running? What’s with that dependency array?

This article will walk you through what useEffect actually does, how to use it correctly, and where developers often get tripped up. We’ll also go over several real-world examples to help you build intuition around how it works.

What Is useEffect?

useEffect is a hook that lets you run side effects in function components. A side effect is anything that affects something outside the scope of the component — like data fetching, timers, subscriptions, or manually manipulating the DOM.

In class components, you would do this kind of logic inside componentDidMount, componentDidUpdate, or componentWillUnmount. With hooks, useEffect handles all of that in one place.

Basic Syntax

useEffect(() => {
  // your side effect logic
}, [dependencies])

The first argument is a function. The second is an array of dependencies. Understanding when the effect runs depends entirely on this array.

Example: Running Once on Mount

This is the most basic use case — run an effect once when the component mounts. Useful for things like analytics or fetching initial data.

useEffect(() => {
  console.log('Component mounted')
}, [])

Notice the empty array. That tells React: only run this effect one time when the component first loads, and never again.

Example: Running on State Change

You can also run an effect whenever some state changes. Let’s say we want to run a side effect every time a counter changes.

const [count, setCount] = useState(0)

useEffect(() => {
  console.log('Count changed:', count)
}, [count])

This effect will run every time count changes. If you leave count out of the dependency array, React won’t know it should re-run the effect.

What Happens Without a Dependency Array?

If you omit the second argument entirely, the effect will run on every render.

useEffect(() => {
  console.log('Runs on every render')
})

This is rarely what you want. It can cause performance issues and unexpected behavior. Always be deliberate with your dependency array.

Cleaning Up: Return Function Inside useEffect

Sometimes your effect sets up something that needs to be torn down later — like a timer or event listener. You can return a function from useEffect to clean up the effect.

useEffect(() => {
  const interval = setInterval(() => {
    console.log('Tick')
  }, 1000)

  return () => {
    clearInterval(interval)
    console.log('Cleaned up')
  }
}, [])

This cleanup function runs when the component unmounts or before the effect re-runs.

Example: Event Listener Cleanup

Let’s say you’re listening to window resize events.

useEffect(() => {
  const handleResize = () => {
    console.log('Window resized:', window.innerWidth)
  }

  window.addEventListener('resize', handleResize)

  return () => {
    window.removeEventListener('resize', handleResize)
  }
}, [])

This pattern ensures that your component doesn’t leak memory or create duplicate listeners when it re-renders.

Common Mistake: Missing Dependencies

A very common mistake is forgetting to include dependencies in the array. For example:

useEffect(() => {
  fetchData(id)
}, [])

If id is a prop that might change, this effect won’t re-run even when it should. The correct version is:

useEffect(() => {
  fetchData(id)
}, [id])

React will warn you about missing dependencies, especially if you use a linter like eslint-plugin-react-hooks. These warnings are important. If you silence them, you risk bugs that are hard to find.

Fetching Data the Right Way

Here’s a realistic example of fetching data based on a dynamic ID prop:

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

  const fetchUser = async () => {
    try {
      const response = await fetch(`/api/user/${id}`, {
        signal: controller.signal,
      })
      const data = await response.json()
      setUser(data)
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Fetch failed:', error)
      }
    }
  }

  fetchUser()

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

This pattern handles dynamic data fetching, cancellation on cleanup, and prevents setting state on unmounted components.

useEffect vs useLayoutEffect

React has another hook called useLayoutEffect. It works the same way as useEffect, but fires earlier — after DOM mutations but before the browser paints.

You only need useLayoutEffect when you’re doing layout measurements or need to block the paint. For most side effects, useEffect is the right choice.

Best Practices

  • Always declare everything you use inside the effect in the dependency array
  • Use cleanup functions to remove timers, subscriptions, or event listeners
  • Avoid declaring functions inside the dependency array manually (e.g. [fetchData]) unless they are stable
  • Don’t abuse useEffect for logic that can happen during render
  • Use useCallback or useMemo to avoid unnecessary re-renders when needed

Debugging Tip: Add Console Logs

Sometimes it's unclear why an effect is running multiple times. You can use simple console logs inside the effect and cleanup to understand the lifecycle.

useEffect(() => {
  console.log('Effect ran')

  return () => {
    console.log('Effect cleaned up')
  }
}, [someState])

Use these logs during development to build your mental model of how useEffect behaves.

Nested useEffects or Multiple Hooks

You can use more than one useEffect in the same component. This helps keep effects organized and focused:

useEffect(() => {
  // Fetch user data
}, [userId])

useEffect(() => {
  // Log user activity
}, [activity])

This is perfectly fine. Hooks always run in the order they’re defined. Keeping effects separate often makes code easier to read and test.

Should You Always Use useEffect?

No. One common trap is overusing useEffect when it's not needed. For example, computing derived state like this:

useEffect(() => {
  setDouble(count * 2)
}, [count])

This can be better handled with useMemo:

const double = useMemo(() => count * 2, [count])

Only reach for useEffect when you truly need a side effect that interacts with the outside world.

Back to Home
HomeExplore
Understanding useEffect in React with Examples