Introduction
useEffect is one of the most important hooks in React. It allows developers to perform side effects in functional components. Side effects are operations that can affect something outside the component itself, such as fetching data from an API, manipulating the DOM, setting up subscriptions, or handling timers.
Before hooks, React developers relied on class component lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount to manage side effects. useEffect unifies these behaviors in a single, versatile hook that works in functional components.
In this article, we will explore:
- What
useEffectis. - How to run effects on mount, update, and unmount.
- How cleanup functions work.
- How dependency arrays affect execution.
- Best practices for using
useEffecteffectively.
What is useEffect?
useEffect is a hook that allows functional components to handle side effects. It runs after the component has rendered, providing a way to interact with external systems or execute operations that depend on the DOM or state.
Syntax
useEffect(() => {
// effect logic
}, [dependencies]);
- The first argument is a function that contains the effect logic.
- The second argument is an optional dependency array that determines when the effect runs.
Basic Example of useEffect
import React, { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Component rendered or count changed");
});
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Explanation
- The effect runs after every render, including the first render.
- Every time the state changes, the component re-renders and triggers the effect.
Running Effects Only on Mount
Sometimes, you want to run an effect only once, similar to componentDidMount in class components. You can achieve this by passing an empty dependency array.
Example
useEffect(() => {
console.log("Component mounted");
// Fetch data or initialize something here
}, []);
- The empty array
[]ensures that the effect runs only once, on initial mount. - Subsequent re-renders do not trigger this effect.
Running Effects on State or Prop Changes
To run an effect whenever a particular state or prop changes, include it in the dependency array.
Example
function Example({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(https://jsonplaceholder.typicode.com/users/${userId})
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]); // effect runs whenever userId changes
return <div>{user ? user.name : "Loading..."}</div>;
}
Explanation
userIdis included in the dependency array.- The effect runs whenever
userIdchanges. - This is useful for fetching new data when props or state change.
Cleanup Functions
Effects may need cleanup to prevent memory leaks or unintended behavior. For example, when using timers, subscriptions, or event listeners.
Example: Timer Cleanup
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
// Cleanup function
return () => clearInterval(interval);
}, []); // Run only once on mount
return <p>Count: {count}</p>;
}
Explanation
- The cleanup function runs before the component unmounts or before the effect runs again.
- This prevents memory leaks and ensures timers, subscriptions, or listeners are properly removed.
Running Effects on Update Only
Sometimes, you want an effect to skip the initial mount and run only on updates. You can do this by tracking a flag or using useRef.
Example
import React, { useState, useEffect, useRef } from "react";
function UpdateOnlyExample() {
const [count, setCount] = useState(0);
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
console.log("Count updated:", count);
}
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Explanation
useRefis used to track whether the component is mounting.- The effect only runs on updates, not on the first render.
Dependency Arrays Explained
The dependency array controls when an effect is executed:
- Empty array
[]: Run once on mount. - No array: Run after every render.
- Array with values
[dep1, dep2]: Run whenever any dependency changes.
Example: Multiple Dependencies
function Example({ a, b }) {
useEffect(() => {
console.log("Effect runs when a or b changes");
}, [a, b]);
}
- The effect runs only when either
aorbchanges. - Avoid including unnecessary dependencies to prevent excessive re-renders.
Common Pitfalls with useEffect
1. Forgetting Dependencies
useEffect(() => {
console.log(count);
}, []); // count is missing, effect may be stale
- Missing dependencies can lead to stale values.
- ESLint’s
react-hooks/exhaustive-depscan help catch these issues.
2. Infinite Loops
useEffect(() => {
setCount(count + 1); // Causes infinite re-renders
}, [count]);
- Avoid updating state directly in the effect without proper checks.
3. Running Effects Too Frequently
- Including too many dependencies can trigger the effect unnecessarily.
- Optimize by minimizing dependencies and using
useCallbackoruseMemowhen passing functions or objects.
Real-World Examples of useEffect
1. Fetching Data from API
function Users({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(https://jsonplaceholder.typicode.com/users/${userId})
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
return <div>{user ? user.name : "Loading..."}</div>;
}
2. Event Listener
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return <p>Window width: {width}</p>;
}
3. Subscriptions
function Chat({ roomId }) {
useEffect(() => {
const unsubscribe = subscribeToRoom(roomId, message => {
console.log(message);
});
return () => unsubscribe();
}, [roomId]);
}
Combining Multiple Effects
It’s often better to separate effects instead of combining everything in one. This makes code easier to read and maintain.
Example
function Example() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// Effect for count
useEffect(() => {
console.log("Count updated:", count);
}, [count]);
// Effect for text
useEffect(() => {
console.log("Text changed:", text);
}, [text]);
}
Best Practices for useEffect
- Keep effects focused on a single responsibility.
- Use dependency arrays correctly to avoid stale closures.
- Include cleanup functions for subscriptions, timers, or listeners.
- Split multiple unrelated effects into separate
useEffecthooks. - Avoid using effects to derive state; compute derived values inside render.
- Use ESLint rules for hooks to catch dependency issues.
Advanced Patterns
1. Fetching Data with AbortController
function User({ userId }) {
useEffect(() => {
const controller = new AbortController();
fetch(https://jsonplaceholder.typicode.com/users/${userId}, {
signal: controller.signal
})
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === "AbortError") console.log("Fetch aborted");
});
return () => controller.abort();
}, [userId]);
}
2. Conditional Effects
useEffect(() => {
if (!userId) return;
console.log("Fetching user data for", userId);
}, [userId]);
- Run effects only when certain conditions are met.
Leave a Reply