← Back to question bank
DebuggingSeniorHard#4022 · 35m

Debug Suspense loading boundaries under interview pressure

A React screen using Suspense loading boundaries passes simple tests but breaks during repeated interaction. Find the likely root cause, patch it, and describe the longer-term design improvement.

Answer Strategy

The broken pattern in this question is "one Suspense boundary at the page root". It looks tidy, but it creates a single point of failure: the slowest data source decides when ANY part of the page renders. Users see a spinner until everything loads, even when the navigation and the fast widgets are already available. The fix is nested boundaries that match the data dependencies of each section.

Locate the boundary by asking "which children depend on which promise?". One boundary per logical data unit. Header, sidebar nav, primary chart, secondary widgets — each gets its own Suspense with a localized fallback. The boundaries are also error-recovery boundaries when paired with ErrorBoundary, so a single slow service can fail without taking down the page.

Adjacent traps: putting Suspense around components that do not actually suspend (no behavior change but adds layout shift on first render), nesting too many boundaries (over-segmented loading skeletons feel choppy), and forgetting that startTransition can keep the previous UI visible during a route change while the next route resolves underneath. The regression test asserts the fast section is visible while the slow one is still in fallback.

Regression Fix: Per-Section Suspense Boundaries

The fixed Dashboard wraps the header and the chart in separate Suspense boundaries; each renders independently as its data resolves.

// THE BUG: a single Suspense boundary wrapped the whole page, so any one
// slow data source held the entire UI in the spinner state. The fix is
// nested boundaries that match the data dependencies of each section, so
// the navigation and the fast widgets render immediately while the slow
// chart shows a localized fallback.

function readResource<T>(promise: Promise<T>): T {
  // Minimal resource-reader shim. In production use a library or React's
  // built-in caching primitives; the contract is "throw the promise".
  const tracked = promise as Promise<T> & { status?: string; value?: T; error?: unknown };
  if (tracked.status === 'fulfilled') return tracked.value as T;
  if (tracked.status === 'rejected') throw tracked.error;
  if (!tracked.status) {
    tracked.status = 'pending';
    promise.then(
      (value) => {
        tracked.status = 'fulfilled';
        tracked.value = value;
      },
      (error) => {
        tracked.status = 'rejected';
        tracked.error = error;
      }
    );
  }
  throw promise;
}

type Page = { title: string };
type Chart = { samples: number[] };

function PageHeader({ pagePromise }: { pagePromise: Promise<Page> }) {
  const page = readResource(pagePromise);
  return <h1>{page.title}</h1>;
}

function SlowChart({ chartPromise }: { chartPromise: Promise<Chart> }) {
  const chart = readResource(chartPromise);
  return <p data-testid="samples">{chart.samples.length} samples</p>;
}

export function Dashboard({
  pagePromise,
  chartPromise,
}: {
  pagePromise: Promise<Page>;
  chartPromise: Promise<Chart>;
}) {
  return (
    <main>
      {/* Header has its own boundary because it is fast and important. */}
      <React.Suspense fallback={<p>Loading header…</p>}>
        <PageHeader pagePromise={pagePromise} />
      </React.Suspense>
      {/* Chart has a separate boundary so it does not block the header. */}
      <React.Suspense fallback={<p role="status">Loading chart…</p>}>
        <SlowChart chartPromise={chartPromise} />
      </React.Suspense>
    </main>
  );
}

Testing Strategy

Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.

Reproduce
Render the broken single-boundary dashboard with a slow chart promise. Assert the user sees a single page-wide spinner until both promises settle. The fix lets the header and the chart resolve independently.
Patch
Add per-section boundaries with localized fallbacks. Test that the fast section renders before the slow section resolves and that each fallback only obscures its own subtree.
Prevent
Add a code-review checklist: "any data source slow enough to suspend gets its own Suspense boundary." Pair with a design-time skeleton mockup so the boundaries also look intentional, not accidental.
import { describe, it, expect } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';

describe('Dashboard suspense regression', () => {
  it('renders the fast header before the slow chart resolves', async () => {
    const pagePromise = Promise.resolve({ title: 'Welcome' });
    let resolveChart: (value: { samples: number[] }) => void = () => {};
    const chartPromise = new Promise<{ samples: number[] }>(
      (resolve) => (resolveChart = resolve)
    );

    render(<Dashboard pagePromise={pagePromise} chartPromise={chartPromise} />);
    // Header resolves on the next tick; chart fallback is visible.
    await waitFor(() => expect(screen.getByText('Welcome')).toBeInTheDocument());
    expect(screen.getByRole('status')).toHaveTextContent('Loading chart');

    resolveChart({ samples: [1, 2, 3] });
    await waitFor(() => expect(screen.getByTestId('samples')).toHaveTextContent('3 samples'));
  });
});

Interviewer Signal

Tests whether you debug from ownership and lifecycle instead of random dependency-array edits.

Constraints

  • State a hypothesis before changing code.
  • Name what evidence would confirm the bug.
  • Avoid broad rewrites unless the current API cannot express the behavior.

Model Answer Shape

  • Reproduce the failing sequence first.
  • Inspect ownership boundaries: local state, props, effects, subscriptions, and server data.
  • Patch the minimal broken boundary and add a regression test.

Tradeoffs

  • A minimal patch reduces risk, but repeated lifecycle bugs often justify a small reducer or custom hook.
  • Adding dependencies can silence lint warnings while still preserving the wrong ownership model.

Edge Cases

  • Double clicks and repeated submissions.
  • Slow network responses arriving out of order.
  • Component remount with stale persisted state.

Testing And Proof

  • Failing interaction sequence.
  • Out-of-order async response.
  • Unmount cleanup.

Follow-Ups

  • What would the code review comment say?
  • What metric or log would show this in production?