Skip to the solution
MediumFrontend6 min read

Explain React Suspense and its use cases.

66 views

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.

  • suspense
  • async
  • performance

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

6 min read Β· 9 sections

Try it in the playground

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

ReactSuspense around a lazy component and a use() read, with no loading flags
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>
  );
}

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 70 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 shadow DOM and virtual DOM?

Continue