Course outline · 0% complete

0/28 lessons0%

Course overview →

Fetching data

lesson 7-3 · ~15 min · 21/28

Fetching data

Data fetching is the classic effect. The standard shape tracks three pieces of state, because a request is always in exactly one of three situations:

function Profile({ userId }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    setError(null);
    fetch("/api/users/" + userId)
      .then(res => {
        if (!res.ok) throw new Error("HTTP " + res.status);
        return res.json();
      })
      .then(setData)
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <p>Loading…</p>;
  if (error) return <p>Error: {error}</p>;
  return <h1>{data.name}</h1>;
}

fetch and promises are straight from Advanced JavaScript. The React part is just: run in an effect keyed on [userId], store the outcome in state, render from state with the early-return pattern from lesson 5-3.

The race condition

Suppose userId changes from 1 to 2 quickly. Two requests are now in flight, and the response for 1 might arrive after the response for 2, overwriting fresh data with stale data. Networks reorder responses all the time.

The fix uses cleanup from lesson 7-2. Each effect run gets its own ignore flag, and the cleanup flips it when the effect becomes outdated:

useEffect(() => {
  let ignore = false;
  fetch("/api/users/" + userId)
    .then(res => res.json())
    .then(json => {
      if (!ignore) setData(json);
    });
  return () => { ignore = true; };
}, [userId]);

When userId changes, React cleans up the old effect, setting the OLD run's ignore to true, so its late response is thrown away. Only the newest run can still write to state.

Code exercise · javascript

Watch the race happen, then watch the fix. fakeFetch resolves after ms milliseconds. In the buggy phase, the slow old query "re" lands after the fast new query "react" and overwrites the screen. The fixed version records the latest query and ignores stale responses. Run it.

async/await in effects

You know async/await from Advanced JavaScript, and the fetch chain above reads more naturally with it. But there is a rule: the effect function itself cannot be async. The reason is the cleanup contract from lesson 7-2, whatever an effect returns is treated as its cleanup function, and an async function always returns a promise, not a function. React would try to call the promise at cleanup time and warn.

The standard shape defines an inner async function and calls it:

useEffect(() => {
  let ignore = false;
  async function load() {
    const res = await fetch("/api/users/" + userId);
    const json = await res.json();
    if (!ignore) setData(json);
  }
  load();
  return () => { ignore = true; };
}, [userId]);

The outer function stays plain, so its return still hands React a real cleanup, and the ignore flag guards against the race exactly as before.

Quiz

In the ignore-flag pattern, when does the old request's ignore become true?

Problem

A fetch effect renders a user profile. The component needs to show three mutually exclusive situations before and after the request. Name the three pieces of state in the standard pattern.

Quiz

Why is useEffect(async () => { ... }, [userId]) wrong?