Skip to the solution
HardFrontend7 min read

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

583 views

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.

  • react
  • data-fetching
  • suspense

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 Β· 9 sections

Try it in the playground

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

ReactThe three patterns side by side, each reporting when its content appeared
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>
  );
}

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 96 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 nextHard

The Activity API β€” hiding UI without unmounting in React 19

Continue