Skip to the solution
MediumFrontend7 min read

How do you fetch data in React?

814 views

The problem

Question presented to candidate: "How would you load data in a React app? Take me through the options and how you would choose."

What a strong answer should cover:

  • React ships no data-fetching solution. That is deliberate β€” it is the common-abstraction principle. The question is really which tool you bring.
  • The four realistic options, roughly in order of preference: a Server Component, a query library (TanStack Query, SWR), use() with Suspense, and a hand-rolled useEffect.
  • Why the effect version is the last resort despite being the one everyone learns first: you must hand-roll loading and error state, race protection, caching, deduplication, refetching, and invalidation.
  • Server state is not UI state. It is a cache of something that lives elsewhere, so it needs staleness handling rather than storage.
  • Waterfalls: fetching sequentially when the requests are independent. Fetch in parallel, or move the fetch up.
  • Render-as-you-fetch versus fetch-on-render, and why starting the request before rendering matters.
  • The framework answer: in Next.js App Router, an async Server Component is usually correct and removes the client-side problem entirely.

Clarifying questions expected:

  • "Is there a framework with server rendering, or is this a pure client SPA?"
  • "Does this data need caching, refetching, or optimistic updates?"
  • "Who else needs the same data?"

Code / implementation expected: Yes β€” the effect version done properly, and ideally the library version for contrast.

  • data fetching
  • useEffect
  • api
  • asynchronous

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 10 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA correct hand-rolled fetch, and the same thing with a tiny cache
import { useState, useEffect, useCallback } from "react";

const DB = {
  1: { id: 1, name: "Ada Lovelace", role: "Mathematician" },
  2: { id: 2, name: "Grace Hopper", role: "Rear Admiral" },
  3: { id: 3, name: "Alan Turing", role: "Logician" },
};
let requestCount = 0;

function fakeFetch(id, signal) {
  requestCount++;
  return new Promise((resolve, reject) => {
    const t = setTimeout(() => {
      if (!DB[id]) reject(new Error("404 not found"));
      else resolve(DB[id]);
    }, 500);
    signal?.addEventListener("abort", () => {
      clearTimeout(t);
      const e = new Error("aborted"); e.name = "AbortError"; reject(e);
    });
  });
}

// ── Hand-rolled: three states, abort on change, swallow AbortError. ────────
function useUserByHand(id) {
  const [state, setState] = useState({ status: "loading", data: null, error: null });

  useEffect(() => {
    const controller = new AbortController();
    setState({ status: "loading", data: null, error: null });

    fakeFetch(id, controller.signal)
      .then((data) => setState({ status: "success", data, error: null }))
      .catch((e) => {
        if (e.name === "AbortError") return;          // deliberate, not a failure
        setState({ status: "error", data: null, error: e.message });
      });

    return () => controller.abort();
  }, [id]);

  return state;
}

// ── A 20-line cache, which is roughly what a query library starts from:
//    deduplication plus reuse. Real ones add staleness, retries, refetching. ─
const cache = new Map();
function useUserCached(id) {
  const [, force] = useState(0);
  const key = String(id);

  useEffect(() => {
    if (cache.has(key)) return;                        // dedupe: already have it
    cache.set(key, { status: "loading" });
    fakeFetch(id)
      .then((data) => cache.set(key, { status: "success", data }))
      .catch((e) => cache.set(key, { status: "error", error: e.message }))
      .finally(() => force((n) => n + 1));
  }, [key, id]);

  return cache.get(key) ?? { status: "loading" };
}

function Card({ title, state }) {
  return (
    <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, flex: 1 }}>
      <div style={{ fontSize: 12, color: "#666" }}>{title}</div>
      {state.status === "loading" && <p>loading…</p>}
      {state.status === "error" && <p style={{ color: "crimson" }}>{state.error}</p>}
      {state.status === "success" && (
        <p><strong>{state.data.name}</strong><br /><span style={{ fontSize: 13 }}>{state.data.role}</span></p>
      )}
    </div>
  );
}

export default function App() {
  const [id, setId] = useState(1);
  const byHand = useUserByHand(id);
  const cached = useUserCached(id);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
        <Card title="hand-rolled effect" state={byHand} />
        <Card title="with a tiny cache" state={cached} />
      </div>
      <p>
        {[1, 2, 3, 99].map((n) => (
          <button key={n} onClick={() => setId(n)} style={{ marginRight: 6 }}>
            user {n}{n === 99 ? " (404)" : ""}
          </button>
        ))}
      </p>
      <p style={{ fontSize: 14 }}>network requests made: <strong>{requestCount}</strong></p>
      <p style={{ color: "#666", fontSize: 13 }}>
        Switch between users you have already loaded. The hand-rolled column
        refetches every time; the cached one does not β€” that difference is most
        of what a query library buys you, before staleness and refetching.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 47 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

What is the difference between controlled and uncontrolled components?

Continue