Dynamic routes
Make one Next.js page for every tea with [slug] folders: catch-all and optional catch-all segments, params as a promise, useParams, and typed routes.
- 01Name the folder for an addressChoose between [slug], [...slug] and [[...slug]], and say what params holds and which addresses answer 404.
- 02Read params the Next.js 16 wayAwait the params promise in a Server Component, list known values or use Suspense, and read them with useParams in a Client Component.
- 03Let TypeScript check your routesType a page with PageProps and turn on typed routes, so a wrong link or a wrong key stops the build.
One folder for every tea
In File-system routing, the tea shop gave each tea a folder of its own: app/teas/green and app/teas/mint, each with the same kind of page inside. That works for two teas. For two hundred, you would copy the same page two hundred times. A dynamic route needs one folder instead. Its name is a placeholder, and Next.js fills it in from the address.
Here is the shop with three teas and one tea page. Like every example in this lesson, it's a real Next.js 16.4.3 project, built with next build and run with next start, and what it sent back was recorded. Read the highlighted lines, then open the Browser tab, pick /teas above the page, and click a tea.
app/teas/[slug] for every teaNext.jsapp/teas/[slug]/page.tsximport { notFound } from "next/navigation";import { getTea, teas } from "../data"; export function generateStaticParams() { return teas.map((tea) => ({ slug: tea.slug }));} export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) { const { slug } = await params; const tea = getTea(slug); if (!tea) notFound(); return ( <main> <h1>{tea.name}</h1> <p> {tea.about} ${tea.price} a box. </p> </main> );}The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
The folder's name is [slug], in square brackets. Square brackets make a folder a dynamic segment: a placeholder that matches any one segment of the address. (A segment is one part of the path between two / characters.) So app/teas/[slug]/page.tsx answers /teas/green, /teas/mint and /teas/oolong. Here is what its file does, line by line:
- Line 8: Next.js calls
TeaPagewith a prop calledparams. Its type,PageProps<"/teas/[slug]">, is one that Next.js writes for this route, so TypeScript knows thatparamsholds aslug. - Line 9 waits for
paramsand takesslugout of it. For/teas/green,slugis"green". Theawaitis there becauseparamsis a promise of the values, not the values themselves. - Line 10 finds the tea with that slug, with
getTeafromapp/teas/data.ts. - Line 11: if no tea has that slug,
notFound()fromnext/navigationstops the page and shows the 404 page instead. - Lines 15 and 17 show the tea's name, its description and its price. The Browser tab shows
Green teaandLight and fresh. $4 a box.for/teas/green. - Lines 4 to 6,
generateStaticParams, return the slugs of the shop's teas. They tell Next.js which teas exist before any visitor asks, so Next.js can make each tea's page during the build.
In app/teas/page.tsx, line 11 builds each tea's link from its slug: /teas/green, /teas/mint and /teas/oolong. Now open the Terminal tab. The route table lists /teas/[slug] once, as one route. Under it are the pages Next.js made from it: ○ /teas/green, ○ /teas/mint and ○ /teas/oolong. The legend under the table says what ○ means: (Static) prerendered as static content. Here, each tea's HTML was made once, during the build, one for each slug on lines 4 to 6. The line ◐ /teas/[slug] is for every other slug, and ◐ means (Partial Prerender) prerendered as static HTML with dynamic server-streamed content: a part made ahead of time, called the shell, with the rest made for each request.
[slug] takes the segment after /teas, and the one page.tsx gets it in params: green, mint or oolong.A tea festival doesn't design a new badge for every guest. It designs one badge with a blank line, and writes each guest's name in the blank. Guests on the list get theirs printed before the doors open. Anyone else gets one written at the door.
- In real life: One badge design with a blank line: "Hello, ___"
- In Next.js: One
page.tsxin a[slug]folder - In real life: The blank line on the badge
- In Next.js: The
[slug]in the folder's name - In real life: The name written on one guest's badge
- In Next.js:
params.slugfor one address, like"green" - In real life: Badges printed before the festival, for the guests on the list
- In Next.js: Pages made during the build, for the slugs
generateStaticParamslists
A folder name in square brackets, like [slug], is a placeholder for one segment of the address. One page in that folder answers every address of that shape, and Next.js passes it the segment's text in the params prop: { slug: "green" } for /teas/green.
Square brackets make a placeholder
The word inside the brackets is up to you, and it becomes the key in params. [slug] gives params.slug; a folder named [teaId] would give params.teaId. Slug is a common name for a short, address-friendly name of a thing, like green or green-tea, so [slug] is the name you'll see most.
A route can have more than one placeholder: one per folder. Each fills one segment, and each adds its own key. Each row is one address from this lesson's recordings, the folder that answered it, and what params held:
| Address | The folder that answers | What params holds |
|---|---|---|
/teas/green | app/teas/[slug] | { slug: "green" } |
/teas/oolong | app/teas/[slug] | { slug: "oolong" } |
/teas/green/reviews/2 | app/teas/[slug]/reviews/[reviewId] | { slug: "green", reviewId: "2" } |
/teas/sale | app/teas/sale | No slug: a folder with a real name wins. |
/teas/green/reviews | None | No page: it answers 404. |
Three rules come out of that table:
- Values are strings. The review page printed
{"slug":"green","reviewId":"2"}:"2"is text, not the number 2. An address is text, so every value in it is too. - A placeholder fills exactly one segment.
/teas/green/reviewshas one segment more than[slug]takes. In the overview's example, pick it above the page: it answered404with the not-found page, and the Network tab shows the status. - A folder with a real name wins. Next.js tries the folders with real names first, and only then the placeholder.
The last rule matters when the shop adds a sale page at /teas/sale, right next to the teas. Here it is, beside [slug]:
[slug]Next.jsapp/teas/sale/page.tsxexport default function SalePage() { return ( <main> <h1>Tea sale</h1> <p>Every tea is $1 off this week.</p> </main> );}Every tea is $1 off this week.
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
app/teas/sale/page.tsx is an ordinary page: line 4 is its heading, Tea sale. The Terminal lists both routes, /teas/[slug] with its three teas and ○ /teas/sale. The Browser shows Tea sale for /teas/sale, so [slug] never saw the word sale, and /teas/green still shows its tea.
/teas/rooibos has the right shape, so [slug] takes it, and the page runs. It finds no tea, and line 11 calls notFound(). Which status the visitor gets follows one rule, from the notFound() docs: when the response is streamed and its headers have already gone out, the status stays 200, and Next.js adds <meta name="robots" content="noindex">, which tells search engines to skip the page. When the not-found answer isn't streamed, it can be a 404.
Here is what that rule gave in this example. Rooibos isn't one of the slugs on lines 4 to 6, so for the first request, Next.js streamed the shell for other slugs, before the page had run. In the overview's example, pick /teas/rooibos, first request, and open the Network tab: the status is 200, and the HTML has the noindex tag. Next.js then kept the finished result in its cache, so the same address, asked for again, was answered from the cache, not streamed, and its status is 404. Both recorded pages are empty: as the notFound() docs describe, React draws the 404 page in the browser, and recordings run no JavaScript. Not found, forbidden and unauthorized covers status codes in full.
Catch-all and optional catch-all segments
The shop wants tea guides, sorted in topics as deep as they need: /guides/brewing, and /guides/brewing/green-tea under it. [slug] takes exactly one segment, so it can't answer both. Two more folder names can:
- A catch-all segment,
[...topic], with three dots inside the brackets, takes one segment or more. - An optional catch-all segment,
[[...topic]], in double brackets, takes zero segments or more: it also answers the folder's own address,/guides.
The page below shows what it gets. Line 15 turns params into text with JSON.stringify, inside a Suspense on lines 7 to 9, so the build has something to show while the page waits for params. Pick a folder name, then each address above the page. Before you click, guess which addresses answer 404.
app/guidesNext.jsapp/guides/[topic]/page.tsximport { Suspense } from "react"; export default function GuidePage({ params }: PageProps<"/guides/[topic]">) { return ( <main> <h1>Tea guides</h1> <Suspense fallback={<p>Loading…</p>}> <ShowParams params={params} /> </Suspense> </main> );} async function ShowParams({ params }: { params: Promise<object> }) { return <p>params: {JSON.stringify(await params)}</p>;}The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
[topic] fills exactly one segment: /guides/brewing gets {"topic":"brewing"}, a string. /guides/brewing/green-tea has one segment too many, and /guides one too few: both answer 404.
[topic] is a plain dynamic segment: /guides/brewing printed params: {"topic":"brewing"}, and the two other addresses answered 404. [...topic] gives an array, one string for each segment: ["brewing"], then ["brewing","green-tea"], while /guides still answers 404. [[...topic]] gives the same arrays, and answers /guides too, with params: {}: no topic at all. When you read it, topic is undefined.
app/guides, and each column one address. A crossed-out card is a 404. Only [[...topic]] answers /guides, and both catch-alls give the deeper address as an array of its segments.Each row is one folder name: the segments it takes, what topic holds, and what /guides answers.
| Folder | Which segments it takes | What topic holds | What /guides answers |
|---|---|---|---|
[topic] | Exactly one segment. | A string: "brewing". | 404 |
[...topic] | One segment or more. | An array: ["brewing", "green-tea"]. | 404 |
[[...topic]] | Zero segments or more. | An array, or no topic at all. | 200, with {} |
Use a catch-all when the depth isn't fixed, like topics inside topics, or a path of folders in a file browser. Use the optional one when the folder's own address should show the same page, like a shop at /shop that filters by /shop/green and /shop/green/loose.
params is a promise
In the tea page, line 9 waits: const { slug } = await params. params isn't the values. It's a promise of them. That's because Next.js can make a page at two moments:
- During the build, before any visitor exists. There's no address yet, so there's no slug, unless your code lists them.
- When a request comes in, with an address, and so with a slug.
A promise lets Next.js see which parts of a page wait for the address, and make everything else during the build. Pages got params as a promise in Next.js 15. Next.js 16 removed the old way, reading params.slug without await, which you'll still see in older tutorials. A Server Component waits with await, which is why TeaPage is async.
New projects have Cache Components on (it's in next.config.ts). With it, Next.js makes as much of every page as it can during the build, and checks that the rest has something to show while it waits. Each button below is one version of the tea page, built with real Next.js. Before you click, guess what the build prints for each.
app/teas/[slug]/page.tsxNext.jsapp/teas/[slug]/page.tsximport { notFound } from "next/navigation";import { getTea } from "../data"; export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) { const { slug } = await params; const tea = getTea(slug); if (!tea) notFound(); return ( <main> <h1>{tea.name}</h1> <p> {tea.about} ${tea.price} a box. </p> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...Error: Route "/teas/[slug]": Next.js encountered uncached or runtime data during prerendering. `fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience. Ways to fix this: - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`) - [block] Set `export const instant = false` to allow a blocking route Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic at body (<anonymous>) at html (<anonymous>)To get a more detailed stack trace and pinpoint the issue, try one of the following: - Start the app in development mode by running `next dev`, then open "/teas/[slug]" in your browser to investigate the error. - Rerun the production build with `next build --debug-prerender` to generate better stack traces.Error occurred prerendering page "/teas/[slug]". Read more: https://nextjs.org/docs/messages/prerender-errorExport encountered an error on /teas/[slug]/page: /teas/[slug], exiting the build.⨯ Next.js build worker exited with code: 1 and signal: nullLine 5 waits for params, and nothing says which teas exist. During the build, no visitor has asked for an address yet, so there's no slug to wait for, and nothing to show meanwhile. Next.js stops the build and names the fixes.
The first version stops the build. Open its Terminal: Route "/teas/[slug]": Next.js encountered uncached or runtime data during prerendering. The page waits for params at the top, and nothing tells Next.js which slugs exist or what to show meanwhile. The error lists the fixes, and the other two versions are the two this lesson needs:
- List the values. With
generateStaticParams, the build runs the page once for each slug it returns, and saves each page:○ /teas/green,○ /teas/mintand○ /teas/oolong. For any other slug, it keeps a shell,◐ /teas/[slug], and runs the page when that request comes in. - Wait inside Suspense. Move the part that awaits
paramsinto its own component, and put it insideSuspense. The build makes the shell with the fallback,Loading the tea…, and streams the tea in for each request:◐ /teas/[slug]. Open the Network tab to see the fallback in the HTML first, and the tea further down.
generateStaticParams names three teas, and Next.js makes a page for each, plus a shell for any other slug. A request for a listed tea gets its page at once. Any other slug gets the shell, ◐ /teas/[slug], and the page runs for that request.Each row is one way to read params, and what the build does with it.
| How the page reads params | What next build does |
|---|---|
await params at the top, nothing else | The build stops: there's no slug yet, and nothing to show while the page waits. |
generateStaticParams lists the teas | Each listed tea's page is made during the build (○). Any other slug gets a shell (◐), filled in on request. |
await params inside Suspense | The shell, with the fallback, is made during the build (◐). The tea streams in for each request. |
A shop that knows its teas lists them. A page whose values can be anything, like a search, waits inside Suspense. generateStaticParams has a lesson of its own, generateStaticParams, with what it does for slugs it doesn't list, and slugs inside slugs.
useParams in Client Components
params is a prop of the page. A component deep inside the page doesn't get it unless you pass it down. A Client Component can read it itself, with the useParams hook. Pages and layouts start as Server Components, which run only on the server. A file that starts with "use client" marks where the client part begins: its components, and everything the file imports, also run in the browser, so they can use hooks and handle clicks. Here, the tea page gets an Add to cart button:
app/teas/[slug]/add-to-cart.tsx"use client"; import { useParams } from "next/navigation"; export default function AddToCart() { const { slug } = useParams<{ slug: string }>(); return ( <button onClick={() => (document.cookie = `cart=${slug}`)}> Add {slug} to cart </button> );}Light and fresh. $4 a box.
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
In app/teas/[slug]/add-to-cart.tsx, line 1 makes it a Client Component, and line 3 imports useParams from next/navigation. Line 6 calls it, and gets the same object as params, with no await: { slug: "green" } on /teas/green. Line 9 saves the slug in a cookie when the button is clicked, and line 10 puts it in the label. Open app/teas/[slug]/page.tsx: line 20 adds <AddToCart /> with no props at all.
The Browser tab shows Add green to cart on /teas/green and Add mint to cart on /teas/mint. That text is in the HTML: the slugs are listed, so Next.js knew each one during the build, and rendered the button with it. The button runs in the browser too, so its code is sent there: the recording checked the JavaScript files the page loads, and they contain cart= from line 9.
The value follows the address as the visitor moves around. Open the Steps tab: these steps were taken in real Chrome. The first opens /teas/green. The second clicks Teas, and the third clicks Mint tea. After the last click, the address is /teas/mint, and the button says Add mint to cart: useParams gave it the new slug. The page didn't load again. After the first step, the steps list no page load, only requests from the page's own JavaScript: Next.js's router asking the server for RSC payloads, the data that describes a page, instead of whole HTML pages. The first look at how Next.js works introduced them.
When the slugs aren't listed, useParams has nothing to give during the build, and it needs a Suspense around it, like await params does: without one, the build stops. Which to use? In a Server Component, read the params prop. In a Client Component deep in the page, call useParams instead of passing the values down through every component. Hooks only run in Client Components (Rules of hooks).
Typed routes check links and params
A route's placeholders are known from its folders, so TypeScript can check them. Next.js writes types for every route during next build (and next dev). Two of them matter here:
PageProps<"/teas/[slug]">, on line 8 of the tea page, is the type of that route's page props. It saysparamsis a promise of{ slug: string }. You never import it: Next.js makes it available in every file, likeLayoutPropsin the root layout.- Typed routes: with
typedRoutes: trueinnext.config.ts, Next.js also writes the list of every route. Then the type check checks the destinations TypeScript can see: thehrefof each Next.jsLink, and the addresses passed torouter.pushandrouter.replace. An address built at run time from a plain string isn't checked unless you mark it with theRoutetype. The option became stable in Next.js 15.5.
next build runs the type check before it makes any page, so a type error stops the build. Pick each version, and read the Terminal's Running TypeScript step.
next.config.tsimport type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ cacheComponents: true, partialPrefetching: true, typedRoutes: true,}; export default nextConfig;$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (8/8) Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found├ ○ /teas└ /teas/[slug] ├ ◐ /teas/[slug] ├ ○ /teas/green ├ ○ /teas/mint └ ○ /teas/oolong ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed contentLine 7 turns on typed routes. During the build, Next.js writes a type that lists every route, and the type check compares each Link address it can see with it. Every link here points at a real route, so the build passes.
With every link right, the type check passes, and the shop builds as before. With <Link href="/tea"> on line 10 of the layout, it stops at that line: Type '"/tea"' is not assignable to type 'UrlObject | RouteImpl<"/tea">'. Did you mean '"/teas"'? And the third version reads id on line 9, from a folder named [slug]. PageProps catches it, with typed routes off: Property 'id' does not exist on type '{ slug: string; }'. Without the type, id would be undefined, and every tea would get the 404 page. A link built with a template literal, like `/teas/${tea.slug}` on line 11 of the teas page, passes the check: it has the shape of /teas/[slug]. (Why TypeScript? explains what a type check is.)
Planning the shop's dynamic routes
To choose a folder for an address, count the segments you don't know ahead of time. Exactly one: [slug]. One or more: [...slug]. Zero or more: [[...slug]]. A segment you do know, like sale, gets a folder with a real name. Each row is one kind of address the shop wants, the file that answers it, and what params holds there, as the recordings showed.
| Address | File | What params holds |
|---|---|---|
/teas/green, /teas/mint, … | app/teas/[slug]/page.tsx | { slug: "green" } |
/teas/sale | app/teas/sale/page.tsx | {}: no dynamic segment. |
/teas/green/reviews/2 | app/teas/[slug]/reviews/[reviewId]/page.tsx | { slug: "green", reviewId: "2" } |
/guides/brewing/green-tea, any depth | app/guides/[...topic]/page.tsx | { topic: ["brewing", "green-tea"] } |
/guides and everything under it | app/guides/[[...topic]]/page.tsx | {} for /guides, an array below it. |
Now sort some addresses. The shop has app/teas/[slug] and app/guides/[...topic]. Which folder takes each address, and which ones have no page at all?
/teas/green/teas/rooibos/guides/brewing/guides/brewing/green-tea/teas/green/reviews/guides
Sort each address: does app/teas/[slug] take it, does app/guides/[...topic] take it, or does it answer 404?
Common mix-ups
- "
paramsis a plain object." No. It's a promise. Await it in a Server Component, or useuseParamsin a Client Component. Readingparams.slugdirectly was removed in Next.js 16. - "
/teas/green/reviews/2givesreviewId: 2." No. Every value is a string: the recording printed"2". Convert it withNumber()when you need a number. - "
[slug]takes the rest of the address." No. It takes exactly one segment./teas/green/reviewsanswered404. - "
[...slug]answers the folder's own address." No. It needs at least one segment:/guidesanswered404. Only[[...slug]]answers it. - "
/teas/salegoes to[slug]." No. A folder with a real name wins over a placeholder in the same folder. - "
notFound()always sends a404." No. When the response is already streaming, its headers are gone, so it keeps200and gets anoindextag. In this example, the first/teas/rooiboswas streamed and answered200; the cached second one answered404.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
The shop adds a page for each review of a tea. A visitor opens /teas/green/reviews/2. What is params.reviewId in app/teas/[slug]/reviews/[reviewId]/page.tsx?
app/
teas/
[slug]/
reviews/
[reviewId]/
page.tsx
page.tsx
data.ts
page.tsx
layout.tsx
page.tsx"2", a string. /teas/green/reviews/2 fills [slug] with green and [reviewId] with 2. The recorded page printed params: {"slug":"green","reviewId":"2"}.
One page in app/guides must answer /guides, /guides/brewing and /guides/brewing/green-tea, and its key must be topic. What's the folder's name?
app/guides/[[...topic]]. Double brackets make the catch-all optional, so it takes zero segments or more. In the playground, it answered /guides with {}, and the deeper addresses with arrays.
This page is app/guides/[...topic]/page.tsx, and the shop has no app/guides/page.tsx. What does /guides answer?
import { Suspense } from "react";
export default function GuidePage({ params }: PageProps<"/guides/[...topic]">) {
return (
<main>
<h1>Tea guides</h1>
<Suspense fallback={<p>Loading…</p>}>
<ShowParams params={params} />
</Suspense>
</main>
);
}
async function ShowParams({ params }: { params: Promise<object> }) {
return <p>params: {JSON.stringify(await params)}</p>;
}
404. A catch-all needs at least one segment, and /guides has none after guides. The recording answered 404 with the not-found page. Rename the folder [[...topic]] to answer it too.
This is app/teas/[slug]/page.tsx, and next build stops with Property 'id' does not exist on type '{ slug: string; }'. What should line 9 read?
import { notFound } from "next/navigation";
import { getTea, teas } from "../data";
export function generateStaticParams() {
return teas.map((tea) => ({ slug: tea.slug }));
}
export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) {
const { id } = await params;
const tea = getTea(id);
if (!tea) notFound();
return (
<main>
<h1>{tea.name}</h1>
<p>
{tea.about} ${tea.price} a box.
</p>
</main>
);
}
import { notFound } from "next/navigation";
import { getTea, teas } from "../data";
export function generateStaticParams() {
return teas.map((tea) => ({ slug: tea.slug }));
}
export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) {
const { slug } = await params;
const tea = getTea(slug);
if (!tea) notFound();
return (
<main>
<h1>{tea.name}</h1>
<p>
{tea.about} ${tea.price} a box.
</p>
</main>
);
}
slug, as in const { slug } = await params. The folder is [slug], so params has no id. The recorded build stopped at line 9: Property 'id' does not exist on type '{ slug: string; }'.
The tea page, now without generateStaticParams, gets the Add to cart button from the useParams section. The build stops. What's the smallest fix?
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { getTea } from "../data";
import AddToCart from "./add-to-cart";
export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
return (
<main>
<Suspense fallback={<p>Loading the tea…</p>}>
<TeaDetails params={params} />
</Suspense>
<AddToCart />
</main>
);
}
async function TeaDetails({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const tea = getTea(slug);
if (!tea) notFound();
return <h1>{tea.name}</h1>;
}
Wrap <AddToCart /> in Suspense, or move it into the one on lines 9 to 11. The recorded build stopped with: Next.js encountered URL data useParams() in a Client Component outside of <Suspense>. Listing the slugs with generateStaticParams fixes it too: the button example built with it.
With these files, next build stops before it makes any page. Which file do you delete to fix it?
app/
shop/
[[...filters]]/
page.tsx
page.tsx
layout.tsx
page.tsxDelete app/shop/page.tsx. The optional catch-all already answers /shop, so the two files are two pages for one address. The recorded build stopped with: You cannot define a route with the same specificity as an optional catch-all route ("/shop" and "/shop/[[...filters]]").
Check your understanding
For each question, count the address's segments, and ask when each value is known: during the build, or on a request.
Question 1 of 8Which folder gives one page to
/teas/green,/teas/mintand every other tea?Choose an answer to see the explanation.
Question 2 of 8What does
paramshold for/guides/brewing/green-teawith this file asapp/guides/[...topic]/page.tsx?Read the code, then predictTSXimport { Suspense } from "react"; export default function GuidePage({ params }: PageProps<"/guides/[...topic]">) { return ( <main> <h1>Tea guides</h1> <Suspense fallback={<p>Loading…</p>}> <ShowParams params={params} /> </Suspense> </main> ); } async function ShowParams({ params }: { params: Promise<object> }) { return <p>params: {JSON.stringify(await params)}</p>; }Choose an answer to see the explanation.
Question 3 of 8With
app/guides/[...topic]/page.tsx, what does/guidesanswer?Choose an answer to see the explanation.
Question 4 of 8The shop has
app/teas/sale/page.tsxandapp/teas/[slug]/page.tsx. What does/teas/saleshow?Choose an answer to see the explanation.
Question 5 of 8Why do you write
await paramsin a Next.js 16 page?Choose an answer to see the explanation.
Question 6 of 8A tea page awaits
paramsat the top, with nogenerateStaticParamsand noSuspense. What doesnext builddo?Choose an answer to see the explanation.
Question 7 of 8Where does
useParamswork?Read the code, then predictTSX"use client"; import { useParams } from "next/navigation"; export default function AddToCart() { const { slug } = useParams<{ slug: string }>(); return ( <button onClick={() => (document.cookie = `cart=${slug}`)}> Add {slug} to cart </button> ); }Choose an answer to see the explanation.
Question 8 of 8Typed routes are on, and the layout has
<Link href="/tea">. The shop only has/teas. What happens?Choose an answer to see the explanation.
Key takeaways
- A folder name in square brackets is a dynamic segment:
app/teas/[slug]answers/teas/greenand every other address with one segment after/teas. - The page gets the values in
params, keyed by the words in the brackets. Every value is a string. [...slug]takes one segment or more, as an array.[[...slug]]takes zero or more, so it also answers the folder's own address. Addresses with a shape no folder takes answer404.paramsis a promise. With Cache Components, list the values withgenerateStaticParams, or wait for them insideSuspense; otherwise the build stops.- Client Components read the same values with
useParams.PagePropstypesparams, andtypedRouteschecks the addresses ofLink,router.pushandrouter.replaceduring the build.
Remember the one-liner.
Brackets make a placeholder, and params tells the page what filled it.
Coming next: Search params and URL state. It reads the part of the address after the ?, for search, filters and pages.