Skip to solution
mediumFrontend

How do you fetch data in React?

814 views
01

Understand 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 fetchinguseEffectapiasynchronous
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes useEffect. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. This doc is the decision landscape; <a href="PASTE_ASYNC_OPS_URL_HERE" tar

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A correct hand-rolled fetch, and the same thing with a tiny cache
Run Playground
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>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 47 of 119 decoded in the React.js track. One more won't hurt.

Back to track