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.
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:
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
Mock the API contract with MSW
5 guided stepsWithout 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
Replace useEffect fetching with TanStack Query
5 guided stepsA 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
Model every panel's state as a discriminated union
5 guided stepsThree 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
Kill layout shift with sized skeletons
5 guided stepsA 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
Simulate empty and broken paths and lock them in with tests
5 guided stepsManually 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
You'll walk away with
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