- Published on
- Reading time
- 5 min read
How useEffect Works in React? Lifecycle, Cleanup, and Optimization
- Authors
- Name
- Dmitriy Dobrynin
Table of Contents
- Intro
- Effects Run After Render
- The Dependency Array: Mount, Update, or Both?
- Cleanup Functions: Why They Matter
- What About Strict Mode?
- Performance Considerations
- Summary: Master the Lifecycle, Master the Effect
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

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:
- React mounts your component.
- It runs your effect.
- It immediately unmounts and runs your cleanup.
- 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!