A component's main job is to describe the screen from its props and state. But sometimes a component must also do something outside that job: talk to a server, set a timer, listen to the window size, or change the page title. These actions are called side effects. React gives us the useEffect hook for them. It is one of the hooks people find hardest, so we go slowly, with a clear mental picture.
What is a side effect?
A side effect is anything a component does that reaches outside its own drawing work. Drawing (rendering) must be pure: the same props and state must always give the same JSX, and drawing must not change anything else. Examples of side effects:
- Fetching data from a server.
- Setting a timer with
setTimeoutorsetInterval. - Adding an event listener to the
window. - Changing
document.title. - Saving something in
localStorage. - Connecting to a chat server (a WebSocket).
You must not do these directly in the body of your component, because the body runs on every draw. Imagine fetching data in the body: the fetch would run on every draw, and when the data arrives you set state, which causes another draw, which fetches again, which loops forever. useEffect lets you say: “do this after the screen is drawn, and only when needed”.
Your first effect
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = 'You clicked ' + count + ' times';
});
return <button onClick={() => setCount(count + 1)}>Clicked {count}</button>;
}
useEffect takes a function. React draws the component first, puts it on the screen, and then runs your function. Here the title of the browser tab changes after each draw. This is the shape of every effect: a function that React calls after drawing.
The dependency array
As written, that effect runs after every draw. Often that is too much. The second argument, the dependency array, tells React when to run it again.
useEffect(() => { /* A */ }); // after every draw
useEffect(() => { /* B */ }, []); // only once, after the first draw
useEffect(() => { /* C */ }, [userId]); // after the first draw, and again when userId changes
Think of the array as the list of things the effect uses. If any item in the list is different from last time, React runs the effect again. If none changed, React skips it.
- No array: run after every draw. Rarely what you want.
- Empty array `[]`: run once when the component first appears (it “mounts”).
- With values: run on the first draw, and again whenever one of the values changes.
An effect that fetches data
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/users/' + userId)
.then((response) => response.json())
.then((data) => setUser(data));
}, [userId]);
if (!user) return <p>Loading...</p>;
return <h2>{user.name}</h2>;
}
When the component first appears, it fetches user number 1. If the parent later passes userId 2, the array has a new value, so React runs the effect again and loads user 2. The next lesson shows a complete and safe version of this, with errors and cancelling.
Cleanup: stop what you started
Some effects start something that keeps running: a timer, a listener, a connection. If you never stop it, you get a leak: timers pile up and old listeners keep running after the component is gone. To stop it, return a cleanup function from the effect. React calls it before running the effect again, and when the component is removed.
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(id); // cleanup
}, []);
return <p>{time.toLocaleTimeString()}</p>;
}
When Clock appears, the interval starts. When Clock is removed from the page, React calls the returned function, and the interval stops. Without the cleanup, the timer would try to change the state of a component that no longer exists.
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <p>The window is {width}px wide.</p>;
}
The order of events
It helps to picture what React does in order for the effect with the userId dependency:
- The component draws for the first time. React shows it on the screen.
- React runs the effect function.
- Later,
userIdchanges, and the component draws again. - Before running the effect again, React runs the cleanup of the previous effect.
- React runs the effect again with the new value.
- When the component is removed, React runs the last cleanup.
Why does my effect run twice in development?
In development, StrictMode (which you saw in main.jsx) deliberately runs each effect, its cleanup and the effect again, once, right after the component first appears. It looks strange, but it is a helpful test: it checks that your cleanup really undoes your effect. If your effect works with this extra round, it will also work when a component appears and disappears many times in real life. This extra run only happens in development and never in the built site.
You might not need an effect
Effects are overused by beginners. Many things people put in an effect have a simpler way:
- Calculating a value from props or state: just calculate it while drawing.
const total = price * quantity;needs no effect and no extra state. - Reacting to a click: put the code in the event handler, not in an effect that watches state.
- Resetting state when a prop changes: give the component a different
key, and React will make a fresh one. - Sending data after a form submit: do it in the submit handler.
Use an effect only to synchronise with something outside React: the network, the browser, a timer, a library. If you are only moving data between your own state and props, you probably do not need one.
Common mistakes
- Missing dependency array: the effect runs after every draw, and if it sets state it can loop forever.
- Lying about dependencies: leaving out a value the effect uses, which gives old data.
- Forgetting cleanup for timers, listeners and connections.
- Making the effect function `async`: an effect must return nothing or a cleanup function, but an async function returns a promise. Put an async function inside the effect and call it.
- Using an effect for something that can be calculated while drawing.
// Right way to use async in an effect
useEffect(() => {
async function load() {
const response = await fetch('/api/data');
setData(await response.json());
}
load();
}, []);
Your task
- Make a component that shows the current window width and cleans up its listener.
- Make a timer that counts seconds and stops when you hide it with a button.
- Change the page title to show the number of items in a to-do list.
- Open the browser console, add a
console.login the effect and its cleanup, and watch the order.
How to check it worked
- The timer stops when the component is removed (no more logs).
- The window width updates as you resize the browser.
- You can say in one sentence what each of the three dependency array forms does.
Summary
useEffectruns code after the screen is drawn, for work outside React such as fetching, timers and listeners.- The dependency array decides when it runs again: none means every draw,
[]means once, values mean when they change. - Return a cleanup function to stop timers and remove listeners.
- Do not use an effect for things you can calculate, or for reacting to clicks.