Back to path
IntermediatePulse · Project 5 of 12 ~6h· 5 milestones

Add routing and split the bundle

Continues from the last build: The last rung wired Pulse to real data and shipped a working create-segment form, but everything still lives on one screen, so the browser downloads metrics code, events code, segments code, and settings code before the user has clicked anything.

Open Pulse in the network tab today and you will see one enormous JavaScript file arrive before a single pixel of the dashboard paints.

React Router v6 data routersCode-splitting with React.lazySuspense fallback UXURL as source of truth for stateuseSearchParamsRoute-level error boundariesBundle analysisPrefetch on hover/intent

What you'll build

Pulse has four routed views with code-split chunks, a date range that survives navigation and reload via the URL, a friendly not-found route, and a route-level error boundary that isolates a crash to the page that caused it.

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/App.tsxtsx
import { useState } from 'react';
import { DateRangePicker } from './components/DateRangePicker';
import { Overview } from './pages/Overview';
import { Events } from './pages/Events';
import { Segments } from './pages/Segments';
import { Settings } from './pages/Settings';

type Tab = 'overview' | 'events' | 'segments' | 'settings';

export function App() {
  const [tab, setTab] = useState<Tab>('overview');
  const [range, setRange] = useState<'7d' | '30d' | '90d'>('7d');

  return (
    <div className="app-shell">
      <nav>
        <button onClick={() => setTab('overview')}>Overview</button>
        <button onClick={() => setTab('events')}>Events</button>
        <button onClick={() => setTab('segments')}>Segments</button>
        <button onClick={() => setTab('settings')}>Settings</button>
      </nav>
      <DateRangePicker value={range} onChange={setRange} />
      {tab === 'overview' && <Overview range={range} />}
      {tab === 'events' && <Events range={range} />}
      {tab === 'segments' && <Segments />}
      {tab === 'settings' && <Settings />}
    </div>
  );
}

Reading this file

  • import { Overview } from './pages/Overview';A static top-level import pulls Overview's whole module graph into the same chunk as App, even on a visit where the user never leaves the Events tab.
  • const [tab, setTab] = useState<Tab>('overview');The active section lives in component state only, so it resets to overview on every reload and cannot be linked to directly.
  • const [range, setRange] = useState<'7d' | '30d' | '90d'>('7d');The date range is plain useState too, this is the exact value this rung moves into the URL so it survives navigation and refresh.
  • {tab === 'overview' && <Overview range={range} />}All four pages are mounted conditionally by a boolean check, not by a route, so there is no URL that maps to 'the events view'.

Everything renders unconditionally, so the browser must parse and run all four pages' code before anything appears, even if the user only ever looks at Overview.

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

The build, milestone by milestone

  1. 1

    Install React Router and define the route tree

    5 guided steps

    A route is a contract with the browser: a URL that always resolves to the same view. Lazy-loading each route's element means the initial bundle only contains the app shell and the Overview page, everything else downloads only when its route is actually visited.

  2. 2

    Preserve the date range across navigation

    4 guided steps

    State that only lives in React memory disappears on refresh and cannot be shared in a Slack message or bookmarked. The URL is the one piece of client state a browser persists and a teammate can paste, so anything the user would want to return to (like which 30 day window they were looking at) belongs there.

  3. 3

    Add a route-level error boundary and a not-found route

    4 guided steps

    Right now a single bad response from the mocked API, or a bug in one chart, can crash the whole React tree because there is no boundary. A route-level error boundary contains the blast radius to the page the user was actually on, and a not-found route turns a broken link into a helpful dead end instead of a white screen.

  4. 4

    Verify the bundle actually split, and prefetch on intent

    4 guided steps

    Lazy loading only pays off if the split actually happened, it is easy to write React.lazy correctly and still end up with one giant chunk because of a stray static import elsewhere. Verifying the real build output is the only way to know the initial payload actually shrank, and prefetching on hover trades a little extra network usage for a navigation that feels instant.

  5. 5

    Polish the loading and transition UX

    4 guided steps

    A Suspense fallback that is shorter than the real content causes a layout shift the instant the real page mounts, which both looks janky and can tank a Cumulative Layout Shift score. Route changes are also invisible to screen reader users unless something explicitly announces them, since focus does not move automatically.

What's inside when you start

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

You'll walk away with

Pulse has four routed URLs (/, /events, /segments, /settings) each backed by a lazily-loaded chunk confirmed in a production build
The date range persists across navigation and page reload via a validated ?range= URL search param
A route-level errorElement isolates a thrown error to the page it happened on, and a wildcard route renders a styled not-found page
Hovering or focusing a nav link visibly prefetches that route's chunk before the click lands

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