Back to path
BeginnerPulse · Project 3 of 12 ~5h· 5 milestones

Wire it to real data with honest states

Continues from the last build: Rung 2 left you with an interactive UI, panels, toggles and a segment list that all respond to clicks, but every number on screen comes from a hard-coded array sitting at the top of a file.

Right now Pulse looks alive but it is lying to you. The metrics panel always shows three tidy numbers, the event stream always has rows, and the segment list never fails, because none of it ever asks a server for anything.

TanStack Query useQueryMSW request mockingDiscriminated union state modelingAbortController and query cancellationCumulative Layout Shift preventionaria-live for async statusQuery keys and cache invalidationTesting async UI with Vitest and Testing Library

What you'll build

You will have replaced every hard-coded array with a TanStack Query hook backed by an MSW-mocked version of the real /api/metrics and /api/events contract, and every panel will render one of five explicit states instead of guessing from booleans, with skeletons sized to match their final content so nothing jumps when data lands.

See how we teach, before you sign up

You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:

src/features/metrics/MetricsPanel.tsxtsx
const FAKE_METRICS = [
  { id: 'dau', label: 'Daily active users', value: 4213, deltaPct: 3.2 },
  { id: 'signups', label: 'New signups', value: 187, deltaPct: -1.4 },
  { id: 'retention', label: '7 day retention', value: 61, deltaPct: 0.8 },
]

export function MetricsPanel() {
  return (
    <section className="metrics-panel">
      <div className="metrics-panel__grid">
        {FAKE_METRICS.map((metric) => (
          <article key={metric.id} className="metric-card">
            <h3>{metric.label}</h3>
            <p className="metric-card__value">{metric.value}</p>
            <span>{metric.deltaPct}%</span>
          </article>
        ))}
      </div>
    </section>
  )
}

Reading this file

  • const FAKE_METRICS = [This in-memory array is exactly what rung 3 replaces with a real fetch through TanStack Query.
  • export function MetricsPanel() {No range or query prop yet, because there is nothing real behind it to parametrize.
  • {FAKE_METRICS.map((metric) => (Only one branch exists: always render success. There is no loading, empty, or error path.
  • <section className="metrics-panel">No aria-live here either, so a screen reader user gets no signal when the content later starts changing.

The current panel: one hard-coded array, one branch, no idea what loading or empty or broken looks like.

That's 1 of 8 explained code blocks in this single project.

The build, milestone by milestone

  1. 1

    Mock the API contract with MSW

    5 guided steps

    Without something that behaves like a real backend, you cannot honestly build loading, empty, or error states, there is nothing to be slow, empty, or broken. MSW intercepts fetch() at the network level, so the exact fetch code you ship to production is what you are testing against locally, nothing is swapped out for a fake fetch function.

  2. 2

    Replace useEffect fetching with TanStack Query

    5 guided steps

    A useEffect fetch has no cache, no deduplication, and no built-in cancellation, every mount refetches from scratch and every unmount can leak a state update into an unmounted component. useQuery gives you a cache keyed by exactly the inputs you declare, request deduplication, and an AbortSignal for free, which is what server state actually needs, as opposed to local UI state like an open dropdown.

  3. 3

    Model every panel's state as a discriminated union

    5 guided steps

    Three independent booleans can produce combinations that make no sense, isLoading true and isError true at the same time is a real bug that boolean soup lets slip through silently. A discriminated union with one status field can only ever be exactly one variant, TypeScript will not let you read metrics off a state that is not 'success'.

  4. 4

    Kill layout shift with sized skeletons

    5 guided steps

    A panel that renders nothing while loading and then suddenly mounts content shifts everything below it, that is a real Cumulative Layout Shift, the same metric Core Web Vitals penalizes a production site for. Reserving height up front, whether via a fixed minHeight, aspect-ratio, or a skeleton with the same row count as typical content, means loading, empty, and success all occupy the same footprint.

  5. 5

    Simulate empty and broken paths and lock them in with tests

    5 guided steps

    Manually toggling a dev-only range selector proves the states work once, today. A test that overrides the MSW handler for a single case and asserts the exact DOM output proves they keep working after the next refactor, without you having to remember to check by hand.

What's inside when you start

3 starter files, ready to clone
5 guided milestones
5 full reference solutions
8 code blocks explained line-by-line
5 "is it working?" checks
4 interview questions it prepares you for

You'll walk away with

MSW handlers mocking GET /api/metrics and GET /api/events to the given contract shape, including delayed success, empty, and 500 branches
useMetrics and useEvents hooks built on useQuery with query keys that include every parameter the request depends on
MetricsPanel and EventsPanel rendering a five-variant discriminated union (idle/loading/empty/error/success) with no boolean-flag state
Sized skeletons and a fixed-height container so no panel causes a layout shift when data arrives
A Vitest and Testing Library suite proving the empty state and the error-plus-retry path both render correctly

This is portfolio-grade. Build it free.

Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.

Start building