The useEffect Hook
The useEffect Hook is one of the most important and frequently used Hooks in React. It is used to perform side effects in functional components — actions that interact with something outside of React's rendering process.
Side effects include things like:
- Fetching data from an API
- Setting up timers or intervals
- Updating the page title
- Adding or removing event listeners
- Interacting with browser storage
These operations cannot be placed directly inside the component function body, because React may call that function multiple times during rendering. useEffect provides a safe place to run these operations after rendering is complete.
Importing useEffect
import { useState, useEffect } from 'react';
Basic Syntax of useEffect
useEffect(() => {
// Side effect code goes here
}, [dependencies]);
useEffect takes two arguments:
- A callback function — the code to run as the side effect
- A dependency array — controls when the effect runs
The Dependency Array — When Does useEffect Run?
The second argument (the dependency array) is critical. It determines when the effect is triggered:
No Dependency Array — Runs After Every Render
useEffect(() => {
console.log("This runs after every render");
});
Omitting the dependency array means the effect runs every time the component re-renders. This is rarely what is needed and can cause performance issues.
Empty Array [] — Runs Only Once (On Mount)
useEffect(() => {
console.log("This runs only once when the component first appears");
}, []);
An empty dependency array means the effect runs once when the component is first added to the page (mounted). This is the most common use case for data fetching.
With Dependencies — Runs When Dependencies Change
useEffect(() => {
console.log("This runs when 'userId' changes");
}, [userId]);
When specific values are placed in the array, the effect re-runs only when those values change. This is useful for reacting to prop or state changes.
Example — Updating the Page Title
import { useState, useEffect } from 'react';
function PageTitleUpdater() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Every time count changes, the effect runs and updates the browser tab title. This is a simple but common real-world use of useEffect.
Example — Fetching Data from an API
Data fetching is one of the most common uses of useEffect. The effect runs once after the component mounts, fetches data, and stores it in state:
import { useState, useEffect } from 'react';
function UserList() {
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);
});
}, []); // Empty array = runs once on mount
if (loading) return <p>Loading users...</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
The empty dependency array ensures the fetch only happens once. The loading state shows feedback to the user while the data is being retrieved.
Cleanup Functions
Some effects need to be cleaned up when a component is removed from the page (unmounted). For example, timers and event listeners should be stopped or removed to prevent memory leaks.
A cleanup function is returned from inside the useEffect callback:
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
// Cleanup function — clears the interval when component unmounts
return () => {
clearInterval(interval);
};
}, []); // Runs once on mount
return <p>Time elapsed: {seconds} seconds</p>;
}
The function returned inside useEffect is the cleanup function. React calls it automatically when the component is removed from the screen, preventing the interval from continuing to run in the background.
Running an Effect Based on a Prop
import { useEffect } from 'react';
function ProfileView({ userId }) {
useEffect(() => {
console.log(`Fetching profile for user ID: ${userId}`);
// Imagine an API call here
}, [userId]); // Re-runs every time userId changes
return <p>Viewing profile for user {userId}</p>;
}
When the parent component changes the userId prop, the effect re-runs and fetches new profile data. The dependency array is the key to controlling this behavior.
Common Mistakes with useEffect
- Missing dependencies: If a variable used inside the effect is not included in the dependency array, the effect may use a stale value.
- Infinite loops: If state is updated inside an effect without proper dependencies, it can create an infinite loop (state changes → effect runs → state changes again).
- Not cleaning up: Forgetting to return a cleanup function from effects with timers or subscriptions causes memory leaks.
Key Points
useEffectis used to run side effects like data fetching, timers, and DOM manipulation.- The dependency array controls when the effect runs: no array (every render),
[](once on mount), or[value](when value changes). - Always include variables used inside the effect in the dependency array.
- Return a cleanup function from the effect to handle resource teardown when the component unmounts.
- Avoid creating infinite loops by being careful about which values trigger re-runs.
The next topic introduces the useRef Hook — a way to access DOM elements and persist values across renders without triggering re-renders.
