Rendering must be pure: a component calculates JSX from its props and state and does nothing else. But apps have to do things beyond drawing: set the page title, start a timer, listen to the window size, connect to a server, fetch data. React calls these side effects. Some belong in event handlers because a user action causes them. Others have no user action behind them: they must happen because the component is on screen. For those, React gives you the useEffect hook.
Your first effect
An effect is a function you pass to useEffect. React calls it after it has put the component on the page:
import { useEffect, useState } from "react";
function App() {
const [count, setCount] = useState(0);
console.log("1. render with count", count);
useEffect(() => {
console.log("2. effect runs after render, count is", count);
});
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
export default App;Count: 0
CONSOLE
1. render with count 0
1. render with count 0
2. effect runs after render, count is 0
2. effect runs after render, count is 0The log shows the order: first the render (the function body), then the effect. (Everything is doubled because StrictMode runs the first render, and its effect, twice in development, as explained below.) Because the effect runs after the render has been committed, it never delays what the user sees. With no second argument, this effect runs after every render.
The dependency array
Running after every render is rarely what you want. The second argument, the dependency array, tells React when the effect needs to run again:
| Call | When it runs |
|---|---|
useEffect(fn) |
after every render |
useEffect(fn, []) |
once, after the first render |
useEffect(fn, [a, b]) |
after the first render, and again after any render where a or b changed |
import { useEffect, useState } from "react";
function App() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
useEffect(() => {
console.log("effect: count changed to", count);
}, [count]);
return (
<div>
<button className="count" onClick={() => setCount(count + 1)}>count {count}</button>
<button className="other" onClick={() => setOther(other + 1)}>other {other}</button>
</div>
);
}
export default App;count 1other 1
CONSOLE
effect: count changed to 0
effect: count changed to 0
effect: count changed to 1Clicking "count" runs the effect again; clicking "other" re-renders the component but, because count did not change, the effect is skipped. React compares each dependency with its previous value using Object.is. The rule for the array is simple: list every reactive value (props, state, variables derived from them) that your effect uses. The next lesson covers this in depth, and the linter helps you.
Effects synchronise with the outside world
The classic examples are all about connecting React to something it does not control:
import { useEffect, useState } from "react";
function App() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize); // cleanup
}, []);
useEffect(() => {
document.title = "Width: " + width;
}, [width]);
return <p>The preview is {width}px wide. Resize the window and watch this number.</p>;
}
export default App;Two effects, two jobs: one subscribes to the browser's resize event, and one keeps document.title in step with the width. The browser window, its events and the title are things outside React, which is exactly what effects are for. Typical uses:
- setting
document.title, - adding and removing event listeners on
windowordocument, - starting and stopping timers and intervals,
- connecting to a chat server or WebSocket,
- fetching data when a screen appears,
- controlling a non-React widget (a map, a chart).
Keep separate concerns in separate effects instead of one big effect, as above. Each effect should describe one way of synchronising.
Cleanup: undoing what the effect did
If the effect starts something, it should usually also stop it. Return a function from the effect: React calls it before running the effect again, and when the component is removed.
import { useEffect } from "react";
function App() {
useEffect(() => {
console.log("connect");
return () => console.log("disconnect"); // cleanup
}, []);
return <p>A component that connects while it is on screen.</p>;
}
export default App;A component that connects while it is on screen.
CONSOLE
connect
disconnect
connectYou might expect just "connect". The Console shows connect, disconnect, connect. This is StrictMode, and it is on purpose.
Why StrictMode runs the effect twice
In development, React mounts the component, immediately runs the cleanup (as if the user had navigated away), and mounts it again. It exists to test that your effect can start, stop and start again without leaving a mess (duplicate connections, leaked listeners). If your cleanup is correct, the extra cycle is invisible; if you forgot it, you will see two listeners or two connections and notice the bug at once. This only happens in development, in the strict wrapper; the production build runs the effect once. Never "fix" the double run by removing StrictMode or by adding a flag. Fix the missing cleanup.
Effects versus event handlers
The most useful question to ask is: what causes this code to run?
| Cause | Where the code goes |
|---|---|
| The user did something (click, type, submit) | event handler |
| The component appeared on screen, or a value it depends on changed | effect |
Sending an order to the server when the user presses "Buy" is an event handler. Fetching the product details because the product page appeared is an effect. Likewise, "when this prop changes, also change this state" is usually not a good use of an effect; calculate the value during rendering instead (see You might not need an effect).
import { useEffect, useState } from "react";
function App() {
const [liked, setLiked] = useState(false);
// Wrong place for this: the click is the cause, not the like state.
// Right place: inside handleClick, below.
function handleClick() {
setLiked(true);
console.log("handler: sending like to server");
}
useEffect(() => {
console.log("effect: liked is", liked);
}, [liked]);
return <button onClick={handleClick}>{liked ? "Liked" : "Like"}</button>;
}
export default App;Liked
CONSOLE
effect: liked is false
effect: liked is false
handler: sending like to server
effect: liked is trueBoth lines print, but only the handler's message is tied to the click. If the "send to server" code lived in the effect, it would also run on the first render (when liked is false), and again whenever the component re-mounted, even though the user did nothing.
What an effect cannot do
- Effects run on the client only. They do not run during server rendering, so they are safe places to touch
windowanddocument. - An effect function cannot be
asyncdirectly, because it must return either nothing or a cleanup function, and anasyncfunction returns a promise. Define an async function inside it and call it (the fetching lesson shows this). - The values an effect uses are from the render that created it (the snapshot idea again). A timer that reads
countsees the count from its own render unless you list it as a dependency or use an updater function.
A complete example: a ticking clock
import { useEffect, useState } from "react";
function App() {
const [time, setTime] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(id); // stop the timer when the component goes away
}, []);
return <p>The time is {time.toLocaleTimeString()}</p>;
}
export default App;The effect starts a timer when the component appears and stops it when it leaves. The empty dependency array is correct because the effect uses no reactive values (setTime is stable and does not need to be listed). Without the cleanup, a timer would keep running after the component disappeared, calling setTime on something that no longer exists and leaking memory. With StrictMode's extra cycle, a missing clearInterval would show up immediately as a clock that ticks twice as fast.
Common mistakes
- Forgetting the dependency array, so the effect runs after every render (and may cause an infinite loop if it sets state).
- Leaving out dependencies, so the effect uses stale values.
- Starting a timer, listener or connection without returning a cleanup.
- Putting event-driven code (sending a form) in an effect.
- Making the effect function
async. - Fixing StrictMode's double effect by hiding it instead of writing a cleanup.
- Using an effect to calculate something that could be computed during rendering.
Exercise
- Write an effect that sets
document.titleto the number of clicks on a button. - Add a
keydownlistener onwindowthat logs the pressed key, and remove it in the cleanup. Check in the Console that StrictMode does not leave two listeners. - Create a countdown from 10 that stops at 0, with an interval that is cleared both when it reaches 0 and when the component unmounts.
- Explain, in one sentence each, why "send a like to the server" is an event handler and "subscribe to window resize" is an effect.
Key points
- An effect is code that runs after the component has rendered, to synchronise it with a system outside React: the page title, a timer, a subscription, a network request.
- useEffect(setup, dependencies) runs setup after every render where a dependency changed; an empty array runs it once after the first render, and no array runs it after every render.
- Effects are not for responding to clicks: do that in event handlers. Use an effect when the component being on screen is the reason for the code.
- In development, StrictMode runs each effect, its cleanup and the effect again once, to check that your effect can start and stop safely.