In React, we often focus on rendering UI based on props
and state
. But real-world apps need more: fetching data from APIs, setting up event listeners, or directly manipulating the DOM. These actions are called side effects because they’re "outside" the pure UI rendering process.
To manage these tasks cleanly and efficiently in functional components, React gives us a powerful tool: the useEffect
Hook.
This article will help you master useEffect
, from the basics to advanced techniques and common pitfalls.
1. What is useEffect and Why is it Important? 🧠
Imagine your component as an actor on stage. Its main job is to perform (render UI) based on the script (props
and state
). But between scenes, it needs to do backstage work: change costumes, prepare props, or take direction.
useEffect
is where you do that "backstage" work.
Technically, useEffect
is a Hook that lets you perform side effects in functional components. It combines the power of class component lifecycle methods like componentDidMount
, componentDidUpdate
, and componentWillUnmount
.
You need useEffect
when you want your component to do something:
- After it first renders (e.g., fetch data from an API).
- After every re-render (e.g., update the page title based on state).
- Right before it’s removed from the DOM (e.g., clean up a subscription or event listener).
2. Syntax and How It Works
The basic syntax of useEffect
looks like this:
import React, { useState, useEffect } from 'react'
function MyComponent() {
// ...
useEffect(() => {
// The callback contains your side effect logic
console.log('Component rendered!')
// (Optional) Cleanup function
return () => {
console.log('Cleanup before next render or unmount.')
}
}, [dependencies]) // Dependency array
// ...
}
Let’s break down the 3 most important parts:
a. The Callback (The Effect)
This is where you write your side effect code. It could be a fetch
call, document.title = ...
, or setInterval
. This function runs after rendering is complete and the browser has painted the UI, so it won’t block the display.
b. Dependency Array – The Heart of useEffect ❤️
This is the most important and most confusing part. The array determines when your effect will re-run. There are 3 main cases:
Case 1: No dependency array
useEffect(() => {
// Runs after EVERY render
console.log('Re-rendered!')
})
The effect runs after the first render and after every re-render, for any reason. This is rarely used because it can cause performance issues or infinite loops if you update state inside the effect.
Case 2: Empty array []
useEffect(() => {
// Runs ONLY ONCE after the first render
fetchData()
}, [])
This is perfect for tasks that should only happen once when the component mounts, like fetching initial data. Equivalent to componentDidMount
.
Case 3: Array with values [prop, state]
const [userId, setUserId] = useState(1)
useEffect(() => {
// Runs on first render, and again EVERY TIME userId changes
fetchUserProfile(userId)
}, [userId])
The effect runs after the first render, and then React watches the values in the array (userId
here). If any value changes between renders, the effect runs again. This is the most powerful and common scenario, equivalent to componentDidUpdate
.
c. Cleanup Function 🧹
Some side effects need to be "cleaned up" when they’re no longer needed, to avoid memory leaks. For example:
- If you set up an
interval
, you need toclearInterval
when the component is destroyed. - If you add an event listener to
window
, you need to remove it.
To do this, just return
a function from inside your effect. React will automatically call this function:
- Before the component unmounts (is removed from the DOM).
- Before the effect runs again on the next render (to clean up the previous effect).
Classic example:
useEffect(() => {
const handleResize = () => console.log('Window resized!')
// Add event listener
window.addEventListener('resize', handleResize)
console.log('Event listener added!')
// Cleanup function
return () => {
window.removeEventListener('resize', handleResize)
console.log('Event listener removed!')
}
}, []) // Runs only once
3. Common Pitfalls and Pro Tips ⚠️
Mastering useEffect
means knowing how to avoid common mistakes.
Pitfall 1: Infinite Loop
A classic beginner mistake.
function MyComponent() {
const [count, setCount] = useState(0)
// ⚠️ ERROR: Infinite loop!
useEffect(() => {
// 1. This effect runs after every render.
// 2. It updates the 'count' state.
// 3. Updating state causes a new render.
// 4. Component re-renders, effect runs again... -> Infinite!
setCount((prevCount) => prevCount + 1)
})
// ...
}
How to fix: Always provide a dependency array! Ask yourself: "When should this effect re-run?" If only once, use []
. If when a value changes, put it in the array.
Pitfall 2: Stale State/Props
This happens when you forget to include a value used in the effect in the dependency array.
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const intervalId = setInterval(() => {
// ⚠️ ERROR: 'count' here is always 0!
// The function "captures" the value of 'count' at the time it was created (first render).
// It will never see updated 'count' values.
console.log(`Count is: ${count}`)
}, 1000)
return () => clearInterval(intervalId)
}, []) // Empty array, so effect never re-runs
// ...
}
How to fix:
- Add
count
to the dependency array:[count]
. This will clean up the old interval and create a new one every timecount
changes. - Use functional updates:
setCount(c => c + 1)
. This way, you don’t need to accesscount
directly, so you don’t need to put it in the dependency array.
✨ Pro tip: Always enable the ESLint rule react-hooks/exhaustive-deps
. It will warn you if you forget a dependency.
Pro Tip 1: Split Effects by Responsibility
Don’t cram all logic into a single useEffect
. If you have unrelated tasks, split them up.
// 👎 NOT RECOMMENDED
useEffect(() => {
fetchData(id)
document.title = `User ${id}`
}, [id])
// 👍 RECOMMENDED
useEffect(() => {
fetchData(id)
}, [id])
useEffect(() => {
document.title = `User ${id}`
}, [id])
This makes your code easier to read, maintain, and follows the Single Responsibility Principle.
Pro Tip 2: Abstract Logic with Custom Hooks
If you find yourself repeating useEffect
logic in many components (e.g., API calls, event listeners), wrap it in a Custom Hook.
// Custom hook: useFetch.js
function useFetch(url) {
const [data, setData] = useState(null)
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then(setData)
}, [url])
return data
}
// Usage in a component
function UserProfile({ userId }) {
const user = useFetch(`/api/users/${userId}`)
// ...
}
Conclusion: useEffect Isn’t Complicated
useEffect
isn’t a complicated concept, but it requires attention to detail. To truly master it, always remember these 3 essentials:
- The Effect function: What should your code do?
- Dependency array: When should your code re-run?
- Cleanup function: What needs to be cleaned up after your effect?
Once you internalize this mindset, useEffect
becomes a powerful tool for building complex, efficient, and memory-leak-free React apps.
Good luck on your React journey!