So far your components have only calculated JSX and reacted to clicks. Sometimes a component also needs to talk to something outside React: start a timer, listen to the window size, or load data from a server when it appears. The React useEffect hook is made for that. It lets you run code after React has updated the page, and clean up when it is no longer needed.
What is an effect?
React splits your code into two kinds. Rendering code calculates the JSX and must stay pure. Event handlers run when the user does something, like clicking. But some work is caused by the component simply being on the screen – for example, a chat box should connect to a server whenever it is visible. That is an effect: code that synchronises your component with an external system.
import { useEffect } from "react";
useEffect(() => {
// setup: runs after the component appears on screen
// (and again whenever a dependency changes)
return () => {
// cleanup (optional): runs before the next setup
// and when the component is removed
};
}, [dependency1, dependency2]);The dependency array
The second argument to React useEffect tells it when to run the effect again. It lists every prop, state value or other value from the component that the effect uses. React compares each one with its value from the previous render and re-runs the effect only if something changed.
| You write | The effect runs |
|---|---|
useEffect(fn, []) | Once after the component first appears (plus cleanup when it is removed) |
useEffect(fn, [userId]) | After the first render, and again whenever userId changes |
useEffect(fn) | After every render – rarely what you want |
Watch out: Do not leave out a dependency to stop an effect from running. If the effect uses userId but the array is empty, the effect keeps using the first userId forever, and your screen shows stale data. In a Vite project, the React ESLint plugin warns you about missing dependencies.
Cleaning up
If your effect starts something that keeps running – an interval, an event listener, a connection – return a cleanup function that stops it. Otherwise each new run adds another timer or listener, and they keep running even after the component is gone. In the clock below, try hiding and showing it: the cleanup clears the interval each time the clock disappears.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A clock that cleans up its interval</title>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
.clock { font-size: 28px; font-variant-numeric: tabular-nums; margin: 8px 0; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React, { useState, useEffect } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id); // cleanup
}, []);
return (
<p className="clock">
{now.toLocaleTimeString("en-IN", { timeZone: "Asia/Kolkata" })} IST
</p>
);
}
function App() {
const [show, setShow] = useState(true);
return (
<div>
<button type="button" onClick={() => setShow(!show)}>
{show ? "Hide clock" : "Show clock"}
</button>
{show && <Clock />}
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>Tip: In a Vite project, the app is wrapped in <StrictMode>. In development only, Strict Mode runs your effect, its cleanup and the effect again when a component first appears. This is deliberate: it reveals effects that forget to clean up. If something breaks because of this double run, fix the cleanup rather than removing Strict Mode.
Fetching data with React useEffect
Loading data when a component appears is a classic use of an effect. A good data-loading component handles three situations: loading, error and success. The example below loads users from JSONPlaceholder, a free fake API for testing, using fetch and async/await as explained in Fetch and Async JavaScript.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Loading users with loading and error states</title>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
.error { color: #b02a37; }
li { margin-bottom: 4px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React, { useState, useEffect } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
async function loadUsers() {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (!res.ok) throw new Error("Server replied with " + res.status);
const data = await res.json();
if (!ignore) setUsers(data);
} catch (err) {
if (!ignore) setError(err.message);
} finally {
if (!ignore) setLoading(false);
}
}
loadUsers();
return () => { ignore = true; };
}, []);
if (loading) return <p role="status">Loading users…</p>;
if (error) return <p className="error">Could not load users: {error}</p>;
return (
<ul>
{users.map(u => <li key={u.id}><strong>{u.name}</strong> – {u.email}</li>)}
</ul>
);
}
function App() {
return (
<div>
<h2>Users</h2>
<UserList />
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>Why the ignore flag? Network requests can finish in any order. If the component disappears, or a dependency changes and a new request starts, the old response might arrive late. The cleanup sets ignore to true, so a stale response never overwrites newer data. This matters most when the effect depends on a value, such as a selected user id. Notice that the button resets the status to “loading” in its click handler, not inside the effect:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Re-fetching when a dependency changes</title>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
button { margin-right: 6px; }
.error { color: #b02a37; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React, { useState, useEffect } from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";
function App() {
const [userId, setUserId] = useState(1);
const [todos, setTodos] = useState([]);
const [status, setStatus] = useState("loading");
useEffect(() => {
let ignore = false;
fetch("https://jsonplaceholder.typicode.com/todos?_limit=4&userId=" + userId)
.then(res => {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
})
.then(data => {
if (!ignore) { setTodos(data); setStatus("done"); }
})
.catch(() => {
if (!ignore) setStatus("error");
});
return () => { ignore = true; };
}, [userId]);
return (
<div>
{[1, 2, 3].map(id => (
<button key={id} type="button" disabled={id === userId} onClick={() => { setUserId(id); setStatus("loading"); }}>
User {id}
</button>
))}
{status === "loading" && <p role="status">Loading…</p>}
{status === "error" && <p className="error">Something went wrong. Please try again.</p>}
{status === "done" && (
<ul>
{todos.map(t => <li key={t.id}>{t.completed ? "Done: " : "To do: "}{t.title}</li>)}
</ul>
)}
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>For larger apps, writing fetch logic in every effect gets repetitive. Frameworks and dedicated data-fetching libraries add caching and avoid duplicate requests. Learn the plain version first – it shows you what those tools do for you.
You might not need an effect
Effects are an escape hatch, not the default tool. If you only need to calculate something from props or state – a filtered list, a total, a full name – calculate it during rendering. If something should happen because the user clicked, put it in the event handler. Use an effect only to synchronise with something outside React. Fewer effects means fewer bugs and faster components.
