Published on
Reading time
5 min read

How useEffect Works in React? Lifecycle, Cleanup, and Optimization

Authors
  • avatar
    Name
    Dmitriy Dobrynin
    Twitter

Table of Contents

Intro

If you’ve been using React and noticed unexpected re-renders or stale data — you’re not alone. Many advanced developers run into subtle bugs and performance bottlenecks. And sometimes it happens because of useEffect misunderstanding on how to use it.

Let's unpack the lifecycle behavior of useEffect and explain why it matters for performance. We would also touch on important practices like react useEffect cleanup function.

Effects Run After Render

React useEffect Lifecycle

The code inside useEffect executes after the component has rendered to the DOM. Think of useEffect as saying: “once React has done painting the UI, now go run this effect.”

This makes useEffect ideal for:

  • Fetching data from APIs
  • Subscribing to events (e.g. resize, scroll)
  • Manipulating the DOM directly
  • Synchronizing external systems like localStorage or analytics

Important note: never use useEffect to compute derived data needed during render — you’re already too late. For that, prefer useMemo or compute values directly in the component.

The Dependency Array: Mount, Update, or Both?

useEffect can run:

  • Once on mount (with [] as the dependency array)
  • Every time any value in the dependency array changes
  • After every render (with no dependency array at all — rarely a good idea)

This means:

useEffect(() => {
  console.log('Run after mount')
}, [])

is equal to componentDidMount, and:

useEffect(() => {
  console.log('Run when count changes')
}, [count])

is equal to componentDidUpdate for count.

If you forget to add a dependency — say, a prop or piece of state your effect uses — your logic may rely on stale values. If you include something unstable (like an inline object or function), your effect may run too often. That’s why understanding dependencies is critical when optimizing useEffect.

Cleanup Functions: Why They Matter

A key part of react useEffect cleanup is returning a function from the effect. This tells React how to “tear down” the effect when it re-runs or when the component unmounts:

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

  // Cleanup: stop the interval
  return () => clearInterval(id)
}, [])

Without proper cleanup, you risk memory leaks, duplicate subscriptions, or dangling timers. Always clean up:

  • Event listeners
  • Subscriptions
  • Timers and intervals
  • Ongoing async operations (with AbortController or flags)

Sometimes you can even cleanup the Redux state or its part to free up the memory allocation. But that's the topic for another dicussion.

In short, every side-effect you set up should also have a teardown plan. Especially in real-world apps where components mount and unmount a lot.

What About Strict Mode?

If you're using React 18+ with Strict Mode, you may notice your useEffect runs twice on mount. That’s intentional. React simulates mounting and unmounting to help you detect cleanup bugs early.

Here’s what happens in development:

  1. React mounts your component.
  2. It runs your effect.
  3. It immediately unmounts and runs your cleanup.
  4. It mounts again and re-runs the effect.

In production, this behavior disappears. But in development, it's a gift: it forces you to write resilient effects that handle mounting and unmounting.

Performance Considerations

Even though effects run after render, what they do matters. If your useEffect sets state, that will trigger another render. This is expected for things like data fetching, but may be problematic if you misuse it.

Common performance traps:

  • Setting state inside useEffect without proper guards
  • Including unstable dependencies (like objects/functions) causing excessive re-runs
  • Running expensive calculations inside the effect instead of memoizing If your app feels sluggish or noisy in dev tools, your useEffect usage might be the culprit.

Summary: Master the Lifecycle, Master the Effect

To use useEffect like a pro, you need to align its behavior with how React renders:

  • Effects run after render — don’t rely on them for values needed during render
  • Use the dependency array to control when effects run — and avoid stale or unstable inputs
  • Always clean up side effects when appropriate — especially listeners, timers, and subscriptions
  • Remember that React Strict Mode will double-invoke your effects in dev to help you write safer code
  • Optimize with memoization and tight dependency arrays to reduce redundant work

Once you understand these patterns, useEffect stops being a source of mystery. It starts becoming a predictable and powerful tool.


Want to go further? In the next post, we’ll dive into common anti-patterns. We will discuss problems like bloated logic and overusing state inside effects. We'll also discuss how custom useEffect hooks can bring clarity to your components.

Stay tuned — and happy refactoring!