Skip to solution
mediumFrontend

Explain React Suspense and its use cases.

66 views
01

Understand the problem

Question presented to candidate: "What is Suspense, what actually triggers it, and what do you use it for today?"

What a strong answer should cover:

  • <Suspense fallback={...}> is a boundary: if any component below it suspends, React shows the fallback until it is ready.
  • It moves loading states from inside each component to a boundary above them, so a component never needs its own isLoading.
  • What triggers it: React.lazy, use() on an unresolved promise, and framework data loaders. Not arbitrary promises, and not a plain useEffect fetch.
  • Behaviour is like an Error Boundary: React walks up to the nearest boundary.
  • Placement is a design decision — it decides the granularity of your loading UI.
  • Streaming SSR: the server sends the shell immediately and streams each boundary as it resolves, and boundaries hydrate independently.
  • useTransition to avoid a fallback flash when updating already-visible content.
  • The main constraint: it does not manage or cache the promise — creating one during render restarts the request every render.

Clarifying questions expected:

  • "Is the data coming from a framework loader or a Server Component, or being fetched client-side?" — that decides whether Suspense is even usable.

Code / implementation expected: Yes — a Suspense boundary around a lazy component and a use() read.

suspenseasyncperformance
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 components and promises. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The suspension behaviour in section 3 was produced by rendering a pending

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Suspense around a lazy component and a use() read, with no loading flags
Run Playground
import { Suspense, lazy, use, useState, useTransition } from "react";

// ── Trigger 1: React.lazy. The boundary covers the chunk download. ─────────
const LazyPanel = lazy(
  () => new Promise((resolve) =>
    setTimeout(() => resolve({
      default: () => (
        <div style={{ background: "#eef", padding: 10, borderRadius: 6 }}>
          Lazy panel — arrived as its own chunk.
        </div>
      ),
    }), 800)),
);

// ── Trigger 2: use() on a promise. Crucially the promise is created OUTSIDE
//    the component. Creating it during render would restart it every render. ─
const cache = new Map();
function getUser(id) {
  if (!cache.has(id)) {
    cache.set(id, new Promise((resolve) =>
      setTimeout(() => resolve({ id, name: "User " + id }), 700)));
  }
  return cache.get(id);
}

// Note what is NOT here: no isLoading, no useEffect, no conditional. The
// component reads the value and returns UI; waiting is the boundary's job.
function UserCard({ id }) {
  const user = use(getUser(id));
  return (
    <div style={{ background: "#efe", padding: 10, borderRadius: 6 }}>
      Loaded <strong>{user.name}</strong>
    </div>
  );
}

function Skeleton({ label }) {
  return (
    <div style={{ background: "#f2f2f2", color: "#888", padding: 10, borderRadius: 6 }}>
      {label}
    </div>
  );
}

export default function App() {
  const [userId, setUserId] = useState(1);
  const [showLazy, setShowLazy] = useState(false);
  const [isPending, startTransition] = useTransition();

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, display: "grid", gap: 12 }}>
      <div>
        <button onClick={() => setShowLazy((s) => !s)}>
          {showLazy ? "Hide" : "Load"} lazy panel
        </button>{" "}
        {/* startTransition keeps the CURRENT card visible while the next one
            loads, instead of collapsing back to the skeleton. */}
        <button onClick={() => startTransition(() => setUserId((n) => n + 1))}>
          Next user (in a transition)
        </button>{" "}
        <button onClick={() => setUserId((n) => n + 1)}>Next user (no transition)</button>
      </div>

      {/* Separate boundaries: each region loads independently. */}
      {showLazy && (
        <Suspense fallback={<Skeleton label="fetching the chunk…" />}>
          <LazyPanel />
        </Suspense>
      )}

      <div style={{ opacity: isPending ? 0.5 : 1, transition: "opacity .2s" }}>
        <Suspense fallback={<Skeleton label="loading user…" />}>
          <UserCard id={userId} />
        </Suspense>
      </div>

      <p style={{ color: "#666", fontSize: 13 }}>
        Compare the two "next user" buttons. Without a transition the card
        collapses to the skeleton; with one the old card stays visible and just
        dims while the next loads.
      </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 70 of 119 decoded in the React.js track. One more won't hurt.

Back to track