Command Palette

Search for a command to run...

Hectal
PHASE 27Advanced ~25 min· topic 6 of 6

Topic 27.6

Ship It

In one line

Milestone 6: protect the app with an error boundary and error reporting, split the archive into its own lazily loaded chunk, build it with Vite, deploy it with an SPA fallback, write a README, and plan what to add next.

Think of it like this

Opening a shop. The shelves are stocked and the till works, but before the doors open you put up a sign with the opening hours, check the fire exit, give a friend a key in case something breaks, and write down what to stock next month. Shipping is that last checklist: the app must start fast, fail gracefully, be findable, and be easy for the next person (often future you) to understand.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Ship
Release the app so real users can use it.
Error boundary
A component that catches rendering errors below it and shows a fallback instead of a blank page.
onCaughtError
A createRoot option (React 19) called for every error an error boundary catches, used for reporting.
Code splitting
Breaking the bundle into files that load only when needed, such as one route's code.
lazy
React's function for a component whose code is loaded on first render with import().
Build
Turning source files into small optimised files in dist/ that a browser can load quickly.
SPA fallback
A host rule that answers any unknown path with index.html so the client-side router can show the right page.
README
The document at the top of a project that explains what it is, how to run it and how it works.

Step by step

01Add the safety net

Wrap the routes in an error boundary and pass error callbacks to createRoot. Test it by throwing on purpose (the first example loads a task with no title).

src/main.jsxwhole filejsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App.jsx";
import { report } from "./monitoring.js";
import "./index.css";

createRoot(document.getElementById("root"), {
  onCaughtError: (error, info) => report(error, info.componentStack),
  onUncaughtError: (error, info) => report(error, info.componentStack),
}).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);

02Lazy-load the archive route

The import moves inside lazy, and Suspense gives a fallback. Vite makes a separate chunk for it.

src/App.jsxwhole filejsx
import { lazy, Suspense } from "react";
import { Routes, Route } from "react-router";
import BoardPage from "./routes/BoardPage.jsx";

const Archive = lazy(() => import("./routes/Archive.jsx"));

export default function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<p role="status">Loading…</p>}>
        <Routes>
          <Route path="/" element={<BoardPage />} />
          <Route path="/archive" element={<Archive />} />
        </Routes>
      </Suspense>
    </ErrorBoundary>
  );
}

03Build and preview

This is Vite 8's output for the starter template (Topic 0.4). Your board's list has one more dist/assets/*.js line for the archive chunk, and different sizes and hashes.

Terminalwhole filebash
$ npm run build

> my-app@0.0.0 build
> vite build

vite v8.3.3 building client environment for production...
transforming...
✓ 20 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html                   0.45 kB │ gzip:  0.29 kB
dist/assets/index-DykytF2W.css    4.10 kB │ gzip:  1.47 kB
dist/assets/index-CyBHeG3D.js   222.52 kB │ gzip: 69.26 kB

✓ built in 314ms

$ npm run preview      # serves dist/ locally: test the real build here

04Deploy with an SPA fallback and cache headers

On Vercel, connect the repository (it detects Vite and publishes dist/) and add vercel.json. On Netlify, put _redirects and _headers in public/. Then open a deep link like /archive directly and reload it: it must load the app, not a 404.

vercel.jsonwhole filejson
{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
  "headers": [
    {
      "source": "/assets/(.*)",
      "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
    }
  ]
}
Deploy with an SPA fallback and cache headersdiagram
Rendering diagram…

05Write the README

Write it for a stranger who has five minutes. Lead with what it does and the demo link; keep decisions short and honest.

06Plan the next version

Pick one item from "what to add next" and run it through RADIO again (27.1): new requirements, where new state lives, API changes, tests. The app grows the way it was built: one milestone at a time.

Try it yourself

  1. 1

    Open the archive first

    In the lazy route example, set initialEntries={["/archive"]} on MemoryRouter. Predict: does the fallback appear now? Which console lines change?

  2. 2

    Reset properly

    In the error boundary example, make Try again useful: pass onReset to the boundary from Board's parent and remove bad tasks there (or give the boundary a key that changes). Predict what happens on Try again before and after.

  3. 3

    Write your README

    Fill the README outline for your own board: one paragraph per section. Ask someone who hasn't seen the code to run it from the README alone.

Code & diagrams

An error boundary and error reporting keep the app alive React 19+ New tab

The script clicks **Load a bad task**. The card throws while rendering, the boundary shows its fallback, `onCaughtError` reports it, and the heading and footer stay. **Try again** would render the bad task and fail again: in a real app, refetch or drop the bad record first.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

report to monitoring: Cannot read properties of undefined (reading 'toUpperCase')
on screen: The board hit a problem. Your tasks are saved.
The archive as a lazily loaded route New tab

React Router wraps navigations in a transition, so while the chunk downloads the board stays on screen instead of flashing the fallback. The fallback appears only when the archive is the first screen: try `<MemoryRouter initialEntries={["/archive"]}>`. The second visit is instant: `lazy` caches the loaded module.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

first screen: Board
while downloading: Board
archive chunk downloaded
then: Archive: 5,000 tasks
second visit, no download: Archive: 5,000 tasks
Netlify instead of Verceltext
# public/_redirects  (copied into dist/ by Vite)
/*    /index.html   200

# public/_headers
/assets/*
  Cache-Control: public, max-age=31536000, immutable
README outlinemarkdown
# Task board

A keyboard-accessible Kanban board built with React 19.
**Live demo:** https://your-app.vercel.app  ·  ![Screenshot](docs/board.png)

## Features
- Add, edit, move (buttons or Alt + ←/→) and delete tasks
- Search and priority filters in the URL (shareable links)
- Saved in localStorage; optimistic moves with rollback on server errors
- 5,000-task archive with a virtualised list, loaded on demand

## Run it
npm install · npm run dev · npm test · npm run build

## How it works
- Folder map (src/state, src/api, src/components, src/routes)
- State plan: tasks in a reducer + external store, filters in the URL, UI details local
- Data flow diagram

## Decisions and trade-offs
- Why an external store over useReducer + useEffect
- Why optimistic moves undo only the failed change

## Accessibility
- Keyboard map, focus rules, live region messages
- Tested with: keyboard only, NVDA + Firefox, VoiceOver + Safari, axe

## What's next
- Drag and drop with keyboard alternative · real API · real-time sync

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Deploy without an SPA fallback

Deploy dist/ to a static host with no rewrite rule, open the app, click Archive, then reload the page.

terminal
$ Browser
── what you'll see ──
404: NOT_FOUND (or the host's own "Page not found" page) instead of the archive.

Break #2

Remove the error boundary

In the first example, render <Board /> without <ErrorBoundary>.

terminal
$ Preview
── what you'll see ──
After the click the whole page is blank: the heading and footer are gone too, and the console shows an uncaught TypeError.

Myth vs fact

Myth

If it works with npm run dev, the build will work too.

Fact

Development and production differ: minification, tree shaking, environment variables baked in at build time, real chunk loading and the host's routing. Always test npm run preview and the deployed URL, including a reload on a deep link.

Myth

Code splitting always makes an app faster.

Fact

Each chunk is another request. Split rarely used routes and heavy features; splitting everything into tiny pieces can make the main flow slower.

Myth

A README is optional for a personal project.

Fact

It is the first thing a reviewer, interviewer or future you reads. A good one shows you can explain decisions, not only write code.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Old chunks can disappear after a deploy while a user still has the old index.html open; their next lazy import fails with a chunk load error. Keep previous assets for a while and catch the error in a boundary that offers a reload (Topic 23.3).

  • ▸

    Add a release version to error reports (import.meta.env or a build-time define) so you can tell which deploy introduced a crash, and upload source maps to the error tracker rather than serving them publicly if your code is private.

  • ▸

    Gate deploys on npm test, lint (oxlint in new Vite projects) and a Playwright smoke test of the main flow with the keyboard. Preview deployments per pull request let reviewers try the change before it merges.

  • ▸

    Check Core Web Vitals on the deployed site (Topic 23.4): the board's LCP is the heading and first cards, INP is dominated by moves and typing in Search, and CLS comes from the loading message pushing content; reserve space for it.

Remember this

  1. 1

    Fail gracefully (Topic 14.5 and 23.5). A rendering error with no error boundary unmounts the whole app: a blank page. Wrap the board in a boundary that shows "The board hit a problem" with a Try again button, so the header and the rest keep working. Report the error: createRoot(container, { onCaughtError, onUncaughtError }) (React 19) receives every error with its component stack, which you send to a monitoring service along with the release version. Upload source maps to that service so the stack points at your source, not minified code.

  2. 2

    Split what most users don't need (Topic 16.8). Most visits never open the archive, so its code (the route and the virtual list) shouldn't be in the first download. const Archive = lazy(() => import("./routes/Archive.jsx")) makes Vite put it in a separate file that downloads on first visit; <Suspense fallback> shows a placeholder while it loads when the archive is the first screen. React Router wraps navigations in a transition, so clicking the Archive link keeps the board on screen until the chunk arrives (the live example shows it).

  3. 3

    Build (Topic 23.1). npm run build runs vite build: it bundles and minifies the code, removes unused exports (tree shaking), turns import() calls into separate chunks, and writes dist/ with index.html and content-hashed files in dist/assets/. The development warnings and StrictMode double calls are gone. npm run preview serves dist/ locally so you can test the real build, including keyboard flows, before deploying.

  4. 4

    Deploy (Topic 23.3). dist/ is static files: any static host works. Two settings matter. The SPA fallback: a request for /archive or /?q=bug must return index.html so the router can handle it (Vercel rewrites, Netlify _redirects with 200, nginx try_files). And caching: hashed files in /assets/ can be cached for a year (immutable), while index.html must be revalidated (no-cache) so users get each new release. Vercel and Netlify build on every push and give each pull request a preview URL.

  5. 5

    The README is the app's front door: what it does (with a screenshot), how to run it (npm install, npm run dev, npm test), how it is built (folder map, state plan, where data lives), the decisions and trade-offs (why a store, why the URL, why optimistic), accessibility notes (keyboard shortcuts, what was tested with which screen reader), and the live demo link. Recruiters and interviewers read it before the code.

  6. 6

    What to add next, each a lesson in itself: drag and drop with keyboard alternatives (dnd-kit); reordering within a column; a real backend (a small API with a database) and TanStack Query for server state (Topic 14.6); sign-in (Topic 21.x for safe token handling); offline support with a service worker and a queue of pending moves; real-time updates between users over WebSockets (Topic 25.5); undo/redo (keep past actions, Topic 25.7); TypeScript across the codebase (Phase 12); a server-rendered version with Next.js (Phase 20); end-to-end tests with Playwright and axe (Topic 17.7).

Explain it without notes

01

What would you check before shipping a React app?

02

Why does a client-side routed app need an SPA fallback, and how do you configure one?

03

How do error boundaries and the root error callbacks work together?

04

Which parts of the task board would you code-split, and why?

Practice

01

Make the error boundary's Try again button reset the board to the saved tasks.

02

Write the nginx location blocks for the task board: hashed assets cached for a year, everything else falling back to index.html.

Trade-offs

  • ↔

    Static hosting (Vercel, Netlify, a CDN) is cheap and simple for a client-rendered app; server rendering (Phase 20) improves first load and SEO but needs a server or a framework host.

  • ↔

    More error boundaries mean smaller blast radius but more fallback UI to design; start with one per route and add more around risky widgets.

Done when you can

  • I can add error boundaries and report errors through createRoot callbacks.

  • I can lazy-load a route and explain what the user sees while it loads.

  • I can build, preview and deploy a Vite app with an SPA fallback and cache headers.

  • I can write a README that explains what, how to run, how it works and why.

  • I can plan the next feature with the same milestone approach.