← React for Beginners

Talking to the outside world: useEffect and fetching

Lesson 10 of 12 · 10 min

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.