useEffect & Side Effects
Rendering must be pure, but real apps also need to do things: start a timer, listen to the window size, connect to a chat server or fetch data. Work that reaches outside React is called a side effect. Event handlers are the first place for side effects. But some effects are not caused by a click; they are needed simply because a component is on the screen. For those, React gives you the useEffect hook.
In this lesson you will write effects, control when they run with the dependency array, clean them up, avoid the common traps, and learn when you do not need an effect at all.
Your first effect
useEffect(setup, dependencies) tells React: "after you have updated the screen, run this setup function". The setup can return a cleanup function, which React calls before running the effect again and when the component is removed.
import { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
function StationClock() {
const [time, setTime] = useState(() => new Date());
useEffect(() => {
console.log("Clock started");
const id = setInterval(() => setTime(new Date()), 1000);
return () => {
clearInterval(id);
console.log("Clock stopped");
};
}, []);
return <p>Jaipur Junction time: {time.toLocaleTimeString("en-IN")}</p>;
}
function App() {
const [show, setShow] = useState(true);
return (
<>
<button onClick={() => setShow(!show)}>{show ? "Hide" : "Show"} clock</button>
{show && <StationClock />}
</>
);
}
createRoot(document.getElementById("root")).render(<App />);Loading the example and then clicking "Hide clock" prints:
Clock started Clock stopped
The empty array [] means the effect runs once when the clock appears. When you hide it, React calls the cleanup, which stops the interval. Without clearInterval, the timer would keep running in the background after the clock is gone, and showing the clock again would start a second one. Every effect that starts something should stop it in its cleanup.
The dependency array
The second argument controls when the effect runs again:
| Dependency array | When the effect runs |
|---|---|
| Not given | After every render |
[] | Once after the component appears (and cleanup when it goes) |
[city, unit] | After the first render, and again whenever city or unit changed |
The rule: list every prop, state variable or value from the component body that the effect uses. The exhaustive-deps rule of the official eslint-plugin-react-hooks linter warns when one is missing, so it is worth enabling in every project. Dependencies are compared with Object.is, just like state.
import { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
function WeatherWidget() {
const [city, setCity] = useState("Kochi");
const [unit, setUnit] = useState("C");
const [clicks, setClicks] = useState(0);
useEffect(() => {
console.log(`Connecting to weather feed for ${city} in °${unit}`);
return () => console.log(`Disconnecting from ${city}`);
}, [city, unit]);
return (
<div>
<select value={city} onChange={(e) => setCity(e.target.value)}>
<option>Kochi</option>
<option>Pune</option>
</select>
<button onClick={() => setUnit(unit === "C" ? "F" : "C")}>Unit: °{unit}</button>
<button onClick={() => setClicks(clicks + 1)}>Unrelated clicks: {clicks}</button>
</div>
);
}
createRoot(document.getElementById("root")).render(<WeatherWidget />);Loading the example, clicking "Unrelated clicks", then choosing Pune prints:
Connecting to weather feed for Kochi in °C Disconnecting from Kochi Connecting to weather feed for Pune in °C
The unrelated click re-renders the component, but city and unit did not change, so the effect is skipped. Changing the city first runs the old cleanup (for Kochi), then the new setup (for Pune). Think of an effect as "keep this external thing in sync with these values", not as "run this on mount".
Pitfalls and how to avoid them
Infinite loops. An effect that sets state which is also its dependency runs forever: useEffect(() => setCount(count + 1), [count]). Also watch for objects and functions created during render: const options = { city } is a new object every render, so [options] changes every time. Depend on the simple values ([city]) or create the object inside the effect.
Stale values from missing dependencies. If an effect reads unit but the array only lists [city], the effect keeps using the unit from an old render. Do not silence the linter; fix the array or restructure the code.
Race conditions when fetching. If the user switches from Kochi to Pune quickly, the Kochi response may arrive after the Pune one and overwrite it. Use a flag that the cleanup sets:
import { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
// A fake server: Kochi answers slowly, Pune answers quickly
function fetchTemperature(city) {
const delays = { Kochi: 1500, Pune: 300 };
const temps = { Kochi: 31, Pune: 24 };
return new Promise((resolve) => setTimeout(() => resolve(temps[city]), delays[city]));
}
function Temperature({ city }) {
const [temp, setTemp] = useState(null);
useEffect(() => {
let ignore = false;
setTemp(null);
fetchTemperature(city).then((t) => {
if (!ignore) setTemp(t);
else console.log(`Ignored a late answer for ${city}`);
});
return () => {
ignore = true;
};
}, [city]);
return <p>{city}: {temp === null ? "loading..." : `${temp}°C`}</p>;
}
function App() {
const [city, setCity] = useState("Kochi");
return (
<>
<button onClick={() => setCity("Kochi")}>Kochi</button>
<button onClick={() => setCity("Pune")}>Pune</button>
<Temperature city={city} />
</>
);
}
createRoot(document.getElementById("root")).render(<App />);Click "Pune" straight after the page loads. Pune shows 24°C, and a second later the console prints:
Ignored a late answer for Kochi
With real fetch, you can also pass an AbortController signal and call controller.abort() in the cleanup. For serious data loading, prefer a framework or a data library, as described in Data Fetching Patterns.
You might not need an effect
Effects are an escape hatch for talking to systems outside React. Many effects in beginner code are unnecessary and make apps slower and buggier:
// Unnecessary: derived data copied into state with an effect
const [fullName, setFullName] = useState("");
useEffect(() => setFullName(`${first} ${last}`), [first, last]);
// Better: calculate during render
const fullName = `${first} ${last}`;- Derived values (totals, filtered lists, formatted text): compute them during render.
- Responses to a user action (send the form, show a toast after "Buy"): put the code in the event handler, where you know what happened.
- Resetting state when a prop changes: give the component a
keyinstead. - Page title: in React 19 you can render
<title>{city} weather</title>inside any component and React places it in the document head; no effect needed.
Common mistakes
- No cleanup. Intervals, event listeners and subscriptions must be removed in the cleanup function.
- Lying about dependencies. Every reactive value the effect reads belongs in the array.
- Making the effect function
async.useEffect(async () => ...)returns a promise instead of a cleanup. Define an async function inside the effect and call it. - Using effects for derived data or event logic. Calculate during render or use event handlers.
- Fetching without handling races. Ignore or abort stale responses in the cleanup.
Key points
useEffect(setup, deps)runs after React updates the screen; use it to sync with outside systems.- Return a cleanup function to undo the setup; React calls it before re-running and on removal.
- No array: every render.
[]: once.[a, b]: whenaorbchanges. - Include every reactive value the effect uses in the dependency array.
- Strict Mode runs setup and cleanup an extra time in development to expose bugs.
- Prefer calculating during render and handling events in handlers; effects are an escape hatch.
Exercise
Write a WindowWidth component that shows the current width of the browser window and updates when the window is resized. Use an effect that adds a resize listener to window and removes it in the cleanup. Also show "Mobile layout" when the width is below 600 pixels, calculated during render (not stored in state).
Show solution
import { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const isMobile = width < 600;
return (
<p>
Window width: {width}px ({isMobile ? "Mobile layout" : "Desktop layout"})
</p>
);
}
createRoot(document.getElementById("root")).render(<WindowWidth />);The effect subscribes once and unsubscribes in the cleanup with the same function. isMobile is derived from width, so it needs no state or effect of its own.
Quick Quiz
1. When does an effect with the dependency array [] run?
An empty array means there is nothing that can change, so the setup runs only after the first render.
2. What is the cleanup function for?
Cleanup stops what the setup started so nothing keeps running in the background.
3. A total can be calculated from items in state. What should you do?
Derived values need no state or effect; computing them during render keeps them in sync.
4. Why does Strict Mode run your effect twice in development?
Mounting, unmounting and mounting again exposes setup code that is not undone properly.
Learn React with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
