Everything so far has been self-contained: data in the file, state in a component, markup
out. This lesson is about the edge of that world, and it is the lesson most often taught
wrongly - including, for years, by React’s own documentation.
An effect synchronises your component with a system outside React. A server, a
subscription, a timer, the browser’s title bar, a chart library that owns its own DOM.
It is not “code that runs when the component appears”. That description is close enough
to be believable and wrong in a way that produces bugs you cannot explain.
The shape of it
import { useEffect } from "react";
useEffect(() => {
document.title = `${notes.length} notes`;
}, [notes.length]);
Two arguments: a function, and a list of dependencies. React runs the function after
rendering, and re-runs it whenever anything in that list has changed since last time.
The document title is a good first example precisely because it is outside React. React
owns what is inside your root <div>; the tab’s title belongs to the browser, and keeping
it in step with your state is exactly the job effects exist for.
The dependency list is not a suggestion. Include everything the function reads from props
or state:
useEffect(() => { ... }, [count]); // re-runs when count changes
useEffect(() => { ... }, []); // runs once after the first render
useEffect(() => { ... }); // runs after EVERY render - almost always wrong
That middle one is the one that gets called “on mount”, and the label is why people go
wrong. [] does not mean “on mount”. It means “this effect depends on nothing, so it never
needs re-running”. The behaviour looks the same and the mental model does not.
Fetching data
Here is a real one, with everything it needs:
import { useEffect, useState } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [status, setStatus] = useState("loading");
useEffect(() => {
const controller = new AbortController();
fetch("https://jsonplaceholder.typicode.com/users", {
signal: controller.signal,
})
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then((data) => {
setUsers(data);
setStatus("ready");
})
.catch((error) => {
if (error.name !== "AbortError") setStatus("error");
});
return () => controller.abort();
}, []);
if (status === "loading") return <p>Loading...</p>;
if (status === "error") return <p>Could not load users.</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Four things in there are not optional, and each one is a bug if you leave it out.
response.ok. fetch does not reject on a 404 or a 500. It rejects on network
failure. A 500 arrives as a perfectly happy response whose body is an error page, and
without this check you will try to render it.
Three states, not two. loading, error, ready. Track only the data and an empty
list means both “still loading” and “there are none”, and the user sees “no results” for
the entire time it is working.
The cleanup function. Returning a function from an effect tells React what to undo.
React calls it before running the effect again, and when the component is removed. Here it
aborts the request, so a response for a component that no longer exists is discarded
rather than setting state on nothing.
The AbortError check. Aborting rejects the promise, with an error whose name is
"AbortError". Without that condition you would be recording a failure for a request you
cancelled on purpose.
Whether anyone sees that failure depends on timing: usually a later request lands a
moment afterwards and overwrites the status, so the screen looks fine. It surfaces
intermittently, on a slow connection, when the replacement request happens to be the
slower of the two. That is what makes it worth guarding against rather than waiting to
notice.
The thing that will confuse you first
Run that and put a console.log inside the effect. In development it logs twice.
Your effect did not run twice by accident and your code is not broken. React’s
<StrictMode> - the wrapper in main.jsx from lesson 2 - deliberately runs every effect,
then its cleanup, then the effect again, on the first mount. In development only, never in
the built version.
It does this to find exactly the bug the cleanup function prevents. An effect that sets up
something and does not tear it down will behave differently the second time, and
StrictMode makes that visible immediately rather than in production. If double-running your
effect breaks something, the effect is missing its cleanup.
The correct response is never to remove <StrictMode>. It is to write the cleanup, which
the fetch above already does.
You probably do not need an effect
The commonest use of useEffect is the one that should not be there at all.
Do not use an effect to compute something from state. This is lesson 9’s rule again:
const [notes, setNotes] = useState([]);
const [count, setCount] = useState(0);
useEffect(() => { // NO
setCount(notes.length);
}, [notes]);
That is a whole extra render to store a number you already had. Just calculate it:
const count = notes.length;
Do not use an effect to respond to a user action. If something should happen because
someone clicked, do it in the click handler:
useEffect(() => { // NO
if (submitted) sendToServer(form);
}, [submitted]);
function handleSubmit(e) { // yes
e.preventDefault();
sendToServer(form);
}
The effect version needs a flag whose only job is to trigger it, and now two things can set
that flag. The handler version says what happens and when, in one place.
Do not use an effect to reset state when a prop changes. Give the component a key
instead - the same key from lesson 5. Change the key and React discards the old instance
and its state, and builds a fresh one:
<Profile key={userId} userId={userId} />
The rule underneath all three: an effect is for reaching outside React. If the code
you are about to write only touches your own state and props, it belongs in a render
calculation or an event handler, not in an effect.
Fetching is a genuine effect, which is why this lesson uses it. Even so, real applications
usually hand that job to a library - lesson 12 names a couple - because caching, retries
and not re-fetching the same thing twice are a lot of work to do well by hand.
Recap
An effect synchronises a component with something outside React, and the dependency array
lists what it reads so React knows when to re-run it. [] means “depends on nothing”, not
“on mount”. Return a cleanup function to undo whatever the effect set up, and expect
StrictMode to run the pair twice in development on purpose - that is how you find a missing
cleanup. Fetching needs a response.ok check, three states rather than two, and an abort
on cleanup. And before writing any effect, check it is not something you could calculate
during render or do in an event handler instead.
Next: putting all of it together into a complete application, starting from a picture.