Lesson 3 — $param segments and $ catch-alls
Your mobile app needs detail screens — /posts/42, /users/ana — and maybe a file browser that swallows any depth like /files/docs/readme. Static routes can't do that. This lesson closes the routing fundamentals by teaching the two dynamic tools: the dynamic segment $param and the splat $.
$ PrefixA folder or file whose name starts with $ captures that URL segment into a param. The text after $ is the param's name:
The generated route path keeps the $: /posts/$postId. When the URL /posts/42 is visited, the router matches it and fills in postId: "42".
$ is the whole convention. $postId means "capture this segment into a param named postId". The name is up to you — $id, $slug, $username all work.
Dynamic segments work at every level of the path. Stack them for nested resources:
URL /posts/42/7 produces params { postId: "42", revisionId: "7" }.
Params are available in two places — the loader (for data fetching) and the component (for rendering):
// routes/posts/$postId/route.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// 1. In a loader — params come as an argument
loader: ({ params }) => fetchPost(params.postId),
// 2. In a component — use the Route hook
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
return <article>Post #{postId}</article>
}
$postId, TanStack's generated route tree knows postId is a param. params.postId is typed as string — and params.foobar is a compile error. This is the payoff over Next.js, where params typing is manual.
$A folder/file named exactly $ is a splat — it captures any remaining URL segments, from that point to the end. The captured tail lands in the special _splat property:
Visiting /files/docs/readme matches this route. The captured path is stored under the _splat key:
// routes/files/$/route.tsx
export const Route = createFileRoute('/files/$')({
component: FileViewer,
})
function FileViewer() {
const { _splat } = Route.useParams()
// _splat === 'docs/readme'
return <p>Viewing: {_splat}</p>
}
$ — not _$. The _ prefix means "pathless layout" (Lesson 2), a completely different thing. A splat is a plain $ segment; its captured value lives in params._splat. (In v1 the value is also mirrored under the key * for backwards compatibility — removed in v2. Prefer _splat.)
$ and not *?The TanStack authors chose $ over the conventional * because asterisks don't play nicely with filenames and CLI tools — exactly the friction you'd hit in a file-based router. So everywhere you'd mentally reach for * in Next.js, write $ in TanStack.
| Next.js | TanStack (directory) | Captures |
|---|---|---|
app/posts/[id]/page.tsx | routes/posts/$id/route.tsx | params.id |
app/users/[username]/page.tsx | routes/users/$username/route.tsx | params.username |
app/files/[...slug]/page.tsx | routes/files/$/route.tsx | params._splat |
Two translations to internalize: square brackets become $, and [...slug] (catch-all) becomes a bare $ folder whose value is _splat, not a named param.
You're building a mobile app with these screens:
/posts/42, with a shared /posts list layout above it/users/ana/files/... (e.g. /files/2024/invoices/q3.pdf)Sketch the directory route tree. Which folders are dynamic? Which is a splat? Then reveal the solution.
Why this works:
$postId and $username capture one segment each — typed params you read with Route.useParams()$ (splat) captures everything after /files/, even multiple slashes, into _splatposts/route.tsx is a normal layout — it does add the /posts segment, so the list and detail share a shellpostId segment?$ prefix marks a dynamic segment. Whatever follows the $ is the param name — here postId.routes/posts/$postId/route.tsx produce?$ is preserved in the generated path. The route is registered as /posts/$postId, then matched against real URLs like /posts/42.postId inside the route component?Route.useParams() hook. Destructure the named param — it's typed as string.({ params }) as their argument, so you write loader: ({ params }) => fetchPost(params.postId).$ is the splat. Not _$ — the _ prefix is a pathless layout, a different feature./files/docs/readme hits routes/files/$/route.tsx. What is params._splat?docs/readme — the leading /files/ is the matched parent path._splat property. (v1 also mirrors it as * for backwards compatibility — removed in v2.)$ instead of * for splats?* is reserved for params* breaks filenames and CLIs* is invalid in a URL$.app/files/[...slug]/page.tsx maps to which TanStack file?routes/files/_$/route.tsxroutes/files/$/route.tsxroutes/files/*/route.tsx[...slug] becomes a bare $ folder. The captured value lands in params._splat, not a named param./posts/$postId/$revisionId — how many params are captured?$-prefixed segment captures its own param. /posts/42/7 yields { postId: "42", revisionId: "7" } — both typed.Primary source: TanStack Router — Routing Concepts (Dynamic Route Segments & Splat / Catch-All sections). Read it after the quiz to deepen your understanding.