Skip to solution
hardFrontend

Compare render-as-you-fetch, fetch-on-render, and fetch-then-render.

583 views
01

Understand the problem

Question presented to candidate: "There are three broad ways to combine fetching and rendering. What are they, and which would you reach for?"

What a strong answer should cover:

  • fetch-on-render: the component renders, then an effect starts the request. Simple, and it creates a waterfall — a child cannot start its request until its parent's has resolved, because the child does not exist yet.
  • fetch-then-render: gather everything first, render once it is all there. No waterfall, but the screen shows nothing until the slowest request lands.
  • render-as-you-fetch: start the requests before or as you render, and let Suspense fill each section in as it arrives. The shell paints immediately and slow sections do not gate fast ones.
  • The distinguishing question is when the request starts relative to the render — not which hook or library you use.
  • Render-as-you-fetch needs the promise to be created outside render (a route loader, a cache, a Server Component), because a promise created during render never settles.
  • Independent Suspense boundaries are what make sections arrive independently; one boundary around everything reintroduces "wait for the slowest".
  • Frameworks implement this for you — route loaders, RSC, preload patterns. Hand-rolling it in a client component is where people get it wrong.

Clarifying questions expected:

  • "Are these requests independent, or does one genuinely need the other's result?" — a real dependency cannot be parallelised away.
  • "Is there a router or framework that can start the fetch on navigation?"

Code / implementation expected: Optional. Showing where the promise is created is the whole distinction.

reactdata-fetchingsuspense
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 senior React interviews — assumes Suspense basics. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. All three patterns in section 3 were executed against React 19.2.8 with

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The three patterns side by side, each reporting when its content appeared
Run Playground
import { useState, useEffect, Suspense, use } from "react";

const DELAY = 900;
const t0 = Date.now();
const at = () => Math.round(Date.now() - t0) + "ms";
const load = (name) => new Promise((r) => setTimeout(() => r(name), DELAY));

// ── 1. FETCH-ON-RENDER ─────────────────────────────────────────────────────
// The child does not exist until the parent's data arrives, so its request
// cannot start any earlier. Two sequential round trips.
function Posts() {
  const [d, setD] = useState(null);
  useEffect(() => { load("posts").then(setD); }, []);
  return <Line label="posts" value={d} />;
}
function OnRender() {
  const [user, setUser] = useState(null);
  useEffect(() => { load("user").then(setUser); }, []);
  return (
    <Panel title="❌ fetch-on-render — a waterfall">
      <Line label="user" value={user} />
      {user && <Posts />}
    </Panel>
  );
}

// ── 2. FETCH-THEN-RENDER ───────────────────────────────────────────────────
// Parallel requests, but nothing renders until BOTH have landed.
function ThenRender() {
  const [data, setData] = useState(null);
  useEffect(() => {
    Promise.all([load("user"), load("posts")]).then(([u, p]) => setData({ u, p }));
  }, []);
  return (
    <Panel title="⚠️ fetch-then-render — parallel, but blank until done">
      {!data ? <em style={{ fontSize: 13 }}>nothing on screen yet…</em> : (
        <>
          <Line label="user" value={data.u} />
          <Line label="posts" value={data.p} />
        </>
      )}
    </Panel>
  );
}

// ── 3. RENDER-AS-YOU-FETCH ─────────────────────────────────────────────────
// Promises created at MODULE SCOPE — before any render. A promise created
// during render would be a new one on every attempt and never settle.
const userP = load("user");
const postsP = load("posts");
function User() { return <Line label="user" value={use(userP)} />; }
function PostsRAYF() { return <Line label="posts" value={use(postsP)} />; }
function AsYouFetch() {
  return (
    <Panel title="✅ render-as-you-fetch — shell first, sections fill in">
      <div style={{ fontSize: 13, color: "#161" }}>shell rendered at {at()}</div>
      {/* SEPARATE boundaries: one around both would wait for the slowest. */}
      <Suspense fallback={<em style={{ fontSize: 13 }}>loading user…</em>}><User /></Suspense>
      <Suspense fallback={<em style={{ fontSize: 13 }}>loading posts…</em>}><PostsRAYF /></Suspense>
    </Panel>
  );
}

function Line({ label, value }) {
  return (
    <div style={{ fontSize: 13 }}>
      {value ? "✓ " + label + " at " + at() : "… waiting for " + label}
    </div>
  );
}
function Panel({ title, children }) {
  return (
    <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 10 }}>
      <strong style={{ fontSize: 13 }}>{title}</strong>
      <div style={{ marginTop: 4 }}>{children}</div>
    </div>
  );
}

export default function App() {
  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 540 }}>
      <OnRender />
      <ThenRender />
      <AsYouFetch />
      <p style={{ fontSize: 13, color: "#666" }}>
        Reload to re-run. Every request takes the same {DELAY}ms. The first
        panel finishes last because its second request cannot start until the
        first finishes. The second finishes sooner but shows nothing at all
        until it does. The third has a shell on screen immediately and each
        section appears on its own.
      </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 96 of 119 decoded in the React.js track. One more won't hurt.

Back to track