call(back)

Which Effect Fetches Data Once on Mount?

Which option loads the users once, when Users first renders? Explain what goes wrong with the others.

// A
useEffect(() => {
  fetch("/api/users").then((res) => res.json()).then(setUsers);
});

// B
useEffect(async () => {
  const res = await fetch("/api/users");
  setUsers(await res.json());
}, []);

// C
useEffect(() => {
  fetch("/api/users").then((res) => res.json()).then(setUsers);
}, []);

// D: no effect at all, in the component body
fetch("/api/users").then((res) => res.json()).then(setUsers);

Hints

Hint 1

An effect with no dependency array runs after every render.

Hint 2

An async function returns a promise, and React expects an effect to return a cleanup function or nothing.

Solution

Show the approach

Answer: C

  • A has no dependency array, so it runs after every render. The state update causes a render, which runs the effect again: a loop.
  • B passes an async function directly. It returns a promise where React expects a cleanup function. Define the async function inside the effect and call it.
  • D fetches in the render body, so every render starts another request.
  • In real code, C also ignores a response that arrives after unmount: set a flag, or abort with an AbortController, in the effect's cleanup.

Worth saying out loud

  • In development, React's Strict Mode mounts effects twice on purpose — a correct cleanup makes that harmless.
  • Data libraries (React Query, SWR) or a framework's loader handle caching, deduplication and races that a bare effect doesn't.

Asked at