React is all about building interactive user interfaces. But sometimes, our components need to interact with the outside world.
For example:
- Fetching data from an API
- Updating the browser tab title
- Setting up timers
- Adding event listeners
- Working with WebSockets
- Subscribing to external services
These operations are called side effects.
In React, the useEffect Hook is used to handle these side effects.
What is useEffect?
useEffect is a built-in React Hook that allows you to perform side effects in functional components.
import { useEffect } from "react";
useEffect(() => {
// Side effect code
}, []);
The useEffect Hook runs after the component renders.
A basic example:
import { useEffect } from "react";
function App() {
useEffect(() => {
console.log("Component rendered");
}, []);
return <h1>Hello React</h1>;
}
export default App;
In this example, the effect runs after the component is mounted.
What is a Side Effect?
A side effect is an operation that happens outside the normal React rendering process.
Common examples include:
- API calls
- Browser document updates
- Timers
- Event listeners
- WebSocket connections
- Local storage operations
For example, updating the document title is a side effect:
import { useEffect } from "react";
function App() {
useEffect(() => {
document.title = "My React App";
}, []);
return <h1>Welcome to React</h1>;
}
export default App;
After the component renders, the browser tab title becomes My React App.
Understanding the Dependency Array
The second argument of useEffect is called the dependency array.
It controls when the effect should run.
1. Without a Dependency Array
useEffect(() => {
console.log("Effect executed");
});
This effect runs after every render.
import { useEffect, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Component rendered");
});
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
export default Counter;
Every time the state changes, the component re-renders and the effect runs again.
2. Empty Dependency Array
useEffect(() => {
console.log("Component mounted");
}, []);
An empty dependency array means the effect runs only after the component is mounted.
This is commonly used for fetching initial data.
useEffect(() => {
fetchData();
}, []);
3. With Dependencies
useEffect(() => {
console.log("Count changed");
}, [count]);
This effect runs whenever the count value changes.
Example: Updating the Document Title
Let's create a counter application:
import { useEffect, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
export default Counter;
Whenever count changes, the browser tab title also changes.
The dependency array is:
[count]
This tells React:
Run this effect whenever
countchanges.
Fetching Data Using useEffect
One of the most common use cases of useEffect is fetching data from an API.
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) {
return <p>Loading...</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default Users;
How does this work?
When the component mounts:
useEffect(() => {
// API call
}, []);
The API request runs.
After the data is received:
setUsers(data);
React updates the state and re-renders the component.
Fetching Data When a Value Changes
Suppose we want to fetch posts for a specific user.
import { useEffect, useState } from "react";
function UserPosts({ userId }) {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`
)
.then((response) => response.json())
.then((data) => setPosts(data));
}, [userId]);
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
))}
</div>
);
}
export default UserPosts;
Whenever userId changes, the effect runs again and fetches the new user's posts.
Cleanup Function in useEffect
Some effects need to be cleaned up when a component is removed from the screen.
Examples include:
- Timers
- Event listeners
- WebSocket connections
- Subscriptions
The cleanup function is returned from the effect.
useEffect(() => {
// Setup
return () => {
// Cleanup
};
}, []);
Example: Timer with Cleanup
import { useEffect, useState } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds((previousSeconds) => previousSeconds + 1);
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return <h1>{seconds} seconds</h1>;
}
export default Timer;
Here, the timer starts when the component mounts.
When the component is unmounted, the cleanup function runs:
clearInterval(intervalId);
This prevents memory leaks.
Example: Event Listener
import { useEffect, useState } from "react";
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return <h1>Window Width: {width}px</h1>;
}
export default WindowWidth;
The event listener is added when the component mounts.
When the component is removed, the listener is removed as well.
Avoid Infinite Loops
Be careful when updating a state variable inside an effect that also depends on that same state.
❌ Bad example:
import { useEffect, useState } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, [count]);
return <h1>{count}</h1>;
}
This creates an infinite loop:
-
countchanges. - The effect runs.
-
setCountchangescount. - The component renders again.
- The effect runs again.
And the cycle continues.
Don't Use useEffect for Everything
A common mistake is using useEffect when it is not necessary.
For example:
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
This is unnecessary.
Instead, simply calculate the value:
const fullName = `${firstName} ${lastName}`;
This is simpler and avoids an unnecessary state update.
useEffect vs Event Handlers
Use an event handler when something happens because of a user action.
For example:
function Button() {
function handleClick() {
console.log("Button clicked");
}
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
You do not need useEffect for this.
Use useEffect when you need to synchronize your component with an external system.
Simple Mental Model
You can remember the React flow like this:
State or Props Change
↓
Component Renders
↓
React Updates the DOM
↓
useEffect Runs
This makes useEffect useful for tasks that should happen after rendering.
When Should You Use useEffect?
Use useEffect when you need to:
- Fetch data from an API
- Update the document title
- Set up timers
- Add event listeners
- Subscribe to external services
- Connect to WebSockets
- Work with browser APIs
- Synchronize with third-party libraries
When Should You Avoid useEffect?
You probably do not need useEffect when:
- You are calculating a value from existing state or props.
- You are responding directly to a user action.
- You can perform the operation during rendering.
- You are using an effect only to update another state unnecessarily.
Conclusion
The useEffect Hook is one of the most important Hooks in React.
The key points to remember are:
-
useEffectis used to handle side effects. - The dependency array controls when the effect runs.
- An empty dependency array runs the effect after the initial mount.
- Dependencies should include values used inside the effect.
- Cleanup functions are important for timers, event listeners, and subscriptions.
- Avoid using
useEffectwhen a simple calculation or event handler is enough.
Once you understand when an effect should run and why it should run, using useEffect becomes much easier.
Happy Coding! 🚀
Top comments (0)