Lesson 2 — Server-rendered list and detail
Fills the @list and @detail slots so the filtered list and the selected invoice’s detail both render on the server, driven entirely by the URL.
Every SaaS app eventually grows a workspace that looks the same: a list of records on one side, the detail of whichever one you picked on the other, and a button that opens a form to add a new one. Invoices, customers, projects, support tickets — the surface is identical. You are going to build the canonical version of it: a list of invoices on the left, the selected invoice’s detail on the right, and a “new invoice” form that opens as a modal with its own real URL.
That last detail is the one decision that defines the whole chapter. The lazy way to build a “new” form is a piece of client state — const [open, setOpen] = useState(false) — and a <Dialog open={open}>. It works in the demo. But a state.open modal has no URL, so a teammate can’t paste a link straight to the open form, a refresh wipes it back to the list, and Cmd+click does nothing. The shape you will build instead makes the URL the source of truth for which view is open, which buys you shareability, refreshability, and Cmd+click for free — with no extra code over the useState version, just a different file layout. You will see exactly how the routing pulls that off in the Modal with a real URL lesson; for now, hold onto the trade: the open state of the modal lives in the URL, not in a useState.
You are not learning new primitives in this project — you are applying them for the first time. Parallel routes, intercepting routes, server-side searchParams, and Suspense streaming were each taught in the App Router chapters; this is where they stop being isolated demos and combine into one surface a team would actually ship. Each bullet leads with the capability an experienced engineer reaches for, then names the App Router feature that delivers it.
default.tsx apiece so each independently-rendered region of the page has a fallback when the URL doesn’t match it.Cmd+click each resolve to the right thing.A single URL has to light up the right combination of regions on the page, and the App Router resolves that mapping through the layout’s named slots. The invoices/layout.tsx shell is a two-column grid that receives three props — children, list, and detail — and the router fills each one based on what the current URL matches. Look at how three different URLs resolve into different slot combinations; this slot topology is the spine of the entire chapter.
@list/page.tsx reads ?status=, renders the filtered list @detail/[id]/page.tsx loads the selected invoice @detail/default.tsx “pick an invoice” empty state (.)new/page.tsx intercepts a soft nav into a Dialog new/page.tsx the full-page twin One URL resolves into a combination of named slots inside the two-column shell — /invoices?status=paid lights @list, /invoices/inv_017 lights @list and @detail/[id], and a soft nav to /invoices/new opens (.)new over the list. The two grey slots are always-present halves no example URL lights here: @detail/default is the empty state, and new/ is the full-page twin. Each slot owns its own loading, error, and not-found boundary and its own default.tsx, so the regions render and fail independently.
Do not worry yet about how interception works, what the (.) prefix means, or how the slots stream — those are the jobs of the next three lessons. Right now you only need the map: one layout shell, two parallel slots that render alongside the page’s children, a detail slot that swaps between a loaded invoice and an empty state, and an intercepting route that has a full-page twin.
This is the first project in the course that begins from a starter rather than from scratch. The reason is a deliberate one. Back in the themed-surface project you stood up the whole toolchain by hand — pnpm, AGENTS.md, the strict tsconfig, Biome, the next-themes <Providers> wrapper — and that was the point of that project. Rebuilding it at the start of every project after would be busywork that teaches nothing new. So from here on, each project chapter carries those decisions forward as a snapshot you clone with degit, and you spend your time on what the chapter is actually about.
Here is what arrives in the starter. The bold files are stubs that ship a placeholder so the app builds and runs from the first clone; each one carries a TODO(L<n>) comment naming the lesson that fills it in, and together they are your work for this chapter. Everything else is provided — read the one-line note on the files a lesson will open, and skip the rest until you get there.
html/body shell with the next-themes <Providers>/ to /invoices'use client' theme provider, carried over from the themed-surface project{ children, list, detail }children slot?status= and renders the filtered list'use client' filter pills that drive the ?status= URL'use client' dialog wrapper that closes on navigationListSkeleton and DetailSkeletoncn() class-merge helperInvoice type, statusSchema, and searchParamsSchemainv_001–inv_030)listInvoices(filters) and getInvoice(id) async readsOne thing is intentionally missing: there is no build-time environment validation here, no DATABASE_URL, no .env. The data is an in-memory fixture of thirty invoices, so the project needs no secrets to run. The env-validation layer (@t3-oss/env-nextjs) shows up later, in the chapters where a real Postgres database arrives — its absence here is by design, not an oversight.
Three implementation lessons turn those stubs into the finished surface, each closing on a state you can confirm in the browser.
Lesson 2 — Server-rendered list and detail
Fills the @list and @detail slots so the filtered list and the selected invoice’s detail both render on the server, driven entirely by the URL.
Lesson 3 — Modal with a real URL
Adds the intercepting modal and its full-page twin so soft navigation opens a dialog while a direct visit, refresh, and Cmd+click open the full page.
Lesson 4 — Independent streaming per slot
Adds a per-slot skeleton so the list and detail each stream to content on their own under a throttled network.
Run these in order. The goal of this lesson is reached when the dev server boots the placeholder shell and the verify gate passes clean.
Get the starter codebase from the project repository, under Chapter 035/start/:
pnpm dlx degit terencicp/react-saas-course-projects/Chapter-035/start list-plus-detaildegit fetches the contents of that folder into a fresh list-plus-detail directory without any git history, so you start from a clean slate. pnpm dlx runs a package without installing it first — it is the pnpm equivalent of npx. The repository is a monorepo with one folder per chapter project, and each project folder has a start/ and a solution/ sibling, so you can diff your work against the reference at any point.
Install the dependencies:
cd list-plus-detail && pnpm installThe versions are pinned and the project requires pnpm 11 and Node 24 or newer. You will see the install run and the node_modules symlinks appear.
Start the dev server:
pnpm devThis boots the Next.js dev server on localhost:3000 with Turbopack, the Next.js 16 default bundler — worth knowing the name so you recognize it in the dev banner and any build errors. The root path redirects to /invoices, and its two-slot shell renders with placeholder text in each slot, because @list/page.tsx, @detail/[id]/page.tsx, and the rest are still TODO(L<n>) stubs. A bare shell is exactly what you should see at this stage.
Run the verify gate:
pnpm verifyThis runs Biome in CI mode, then next typegen, then tsc --noEmit, then a full production build — the same gate your CI runs on every pull request, and the first proof that the project is shippable. (pnpm build on its own runs only the build step; verify is the whole gate.)
There are no environment variables to set — the in-memory fixture needs none, and the database that introduces DATABASE_URL arrives in a later unit. When pnpm dev serves the placeholder /invoices shell and pnpm verify completes cleanly, you have the project’s floor in place — a runnable, type-clean starter that is worth committing as your first milestone before you build anything on top of it.