Skip to content

Commit 860c45c

Browse files
committed
docs: document variadic path parameters
Adds the variadic section to the path-params guide and routing concepts, includes optional and variadic parameters in the route matching order (optional parameters were missing from it), and maps Next.js optional catch-all segments to variadic parameters in the migration guide.
1 parent 1706e55 commit 860c45c

4 files changed

Lines changed: 154 additions & 8 deletions

File tree

docs/router/guide/path-params.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,97 @@ function PostsComponent() {
721721

722722
<!-- ::end:framework -->
723723

724+
## Variadic Path Parameters
725+
726+
A regular path param matches exactly one segment. A variadic path parameter, written `{...$paramName}`, matches **zero or more** segments and provides them back to you as an array. The path can keep going after it, so it also works between fixed parts of the URL:
727+
728+
- `files/{...$path}/preview`
729+
- `$bucket/{...$folders}/$file`
730+
731+
Let's build a route for files that can be nested in arbitrarily deep folders:
732+
733+
<!-- ::start:framework -->
734+
735+
# React
736+
737+
```tsx title="src/routes/$bucket/{...$folders}/$file.tsx"
738+
import { createFileRoute } from '@tanstack/react-router'
739+
740+
export const Route = createFileRoute('/$bucket/{...$folders}/$file')({
741+
loader: ({ params }) => {
742+
// params.folders is an Array<string>
743+
return fetchFile([params.bucket, ...params.folders, params.file])
744+
},
745+
component: FileComponent,
746+
})
747+
748+
function FileComponent() {
749+
const { bucket, folders, file } = Route.useParams()
750+
return <div>{[bucket, ...folders, file].join(' / ')}</div>
751+
}
752+
```
753+
754+
# Solid
755+
756+
```tsx title="src/routes/$bucket/{...$folders}/$file.tsx"
757+
import { createFileRoute } from '@tanstack/solid-router'
758+
759+
export const Route = createFileRoute('/$bucket/{...$folders}/$file')({
760+
loader: ({ params }) => {
761+
// params.folders is an Array<string>
762+
return fetchFile([params.bucket, ...params.folders, params.file])
763+
},
764+
component: FileComponent,
765+
})
766+
767+
function FileComponent() {
768+
const params = Route.useParams()
769+
return (
770+
<div>
771+
{[params().bucket, ...params().folders, params().file].join(' / ')}
772+
</div>
773+
)
774+
}
775+
```
776+
777+
<!-- ::end:framework -->
778+
779+
This one route matches `/media/kyoto.jpg`, `/media/photos/kyoto.jpg` and `/media/photos/2026/kyoto.jpg`, binding `folders` to `[]`, `['photos']` and `['photos', '2026']` respectively. The array is never `undefined` since a variadic that matched nothing gives you an empty array. Matching is non-greedy, so the variadic consumes as few segments as the rest of the path allows, and static, required or optional segments in the same position always win over it. Each matched segment is percent-decoded on its own, so an encoded `/` inside a folder name stays inside that folder's value.
780+
781+
When navigating, pass an array for the variadic param. An empty (or omitted) array drops the segment entirely:
782+
783+
```tsx
784+
function Component() {
785+
return (
786+
<div>
787+
{/* navigates to /media/photos/2026/kyoto.jpg */}
788+
<Link
789+
to="/$bucket/{...$folders}/$file"
790+
params={{
791+
bucket: 'media',
792+
folders: ['photos', '2026'],
793+
file: 'kyoto.jpg',
794+
}}
795+
>
796+
Kyoto Photo
797+
</Link>
798+
799+
{/* navigates to /media/kyoto.jpg */}
800+
<Link
801+
to="/$bucket/{...$folders}/$file"
802+
params={{ bucket: 'media', folders: [], file: 'kyoto.jpg' }}
803+
>
804+
Kyoto Photo (root)
805+
</Link>
806+
</div>
807+
)
808+
}
809+
```
810+
811+
A route path can contain up to three variadic segments, as long as consecutive ones are separated by at least one static segment; they resolve left to right, each consuming as little as possible. Prefixes and suffixes are not supported on variadic segments, and a variadic must be separated from a splat by at least one static segment.
812+
813+
> 🧠 Use a variadic parameter when segments _between_ two known parts of the URL vary in depth. If you just want to capture "the rest of the URL" with no routes after it, a [splat route](../routing/routing-concepts.md#splat--catch-all-routes) (`$`) is simpler.
814+
724815
## Internationalization (i18n) with Optional Path Parameters
725816

726817
Optional path parameters are excellent for implementing internationalization (i18n) routing patterns. You can use prefix patterns to handle multiple languages while maintaining clean, SEO-friendly URLs.

docs/router/routing/route-matching.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ When TanStack Router processes your route tree, all of your routes are automatic
99
- Index Route
1010
- Static Routes (most specific to least specific)
1111
- Dynamic Routes (longest to shortest)
12+
- Optional Parameter Routes
13+
- Variadic Parameter Routes
1214
- Splat/Wildcard Routes
1315

1416
Consider the following pseudo route tree:

docs/router/routing/routing-concepts.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,58 @@ This route matches `/posts`, `/posts/tech`, and `/posts/tech/hello-world`.
325325

326326
> 🧠 Routes with optional parameters are ranked lower in priority than exact matches, ensuring that more specific routes like `/posts/featured` are matched before `/posts/{-$category}`.
327327
328+
## Variadic Path Parameters
329+
330+
Variadic path parameters match **zero or more** URL segments in the middle of a path and bind them as an `Array<string>`. They use the `{...$paramName}` syntax.
331+
332+
<!-- ::start:framework -->
333+
334+
# React
335+
336+
```tsx title="src/routes/files.{...$path}.preview.tsx"
337+
// `{...$path}` matches zero or more segments, so this route matches
338+
// `/files/preview`, `/files/docs/preview` and `/files/a/b/c/preview`
339+
import { createFileRoute } from '@tanstack/react-router'
340+
341+
export const Route = createFileRoute('/files/{...$path}/preview')({
342+
component: PreviewComponent,
343+
})
344+
345+
function PreviewComponent() {
346+
const { path } = Route.useParams()
347+
348+
// `path` is an Array<string>: [] for `/files/preview`,
349+
// ['a', 'b', 'c'] for `/files/a/b/c/preview`
350+
return <div>Previewing {path.join('/') || 'the root folder'}</div>
351+
}
352+
```
353+
354+
# Solid
355+
356+
```tsx title="src/routes/files.{...$path}.preview.tsx"
357+
// `{...$path}` matches zero or more segments, so this route matches
358+
// `/files/preview`, `/files/docs/preview` and `/files/a/b/c/preview`
359+
import { createFileRoute } from '@tanstack/solid-router'
360+
361+
export const Route = createFileRoute('/files/{...$path}/preview')({
362+
component: PreviewComponent,
363+
})
364+
365+
function PreviewComponent() {
366+
const { path } = Route.useParams()
367+
368+
// `path` is an Array<string>: [] for `/files/preview`,
369+
// ['a', 'b', 'c'] for `/files/a/b/c/preview`
370+
return <div>Previewing {path().join('/') || 'the root folder'}</div>
371+
}
372+
```
373+
374+
<!-- ::end:framework -->
375+
376+
A variadic segment consumes as few segments as the rest of the pattern allows, and each matched segment is percent-decoded independently. A route path may contain up to three variadic segments as long as consecutive ones are separated by at least one static segment. Prefixes and suffixes are not supported on variadic segments. See [Path Params](../guide/path-params.md#variadic-path-parameters) for details.
377+
378+
> 🧠 Variadic parameter routes are ranked below dynamic and optional parameter routes and above splat routes: `/docs/$section/end` and `/docs/{-$section}/end` both win over `/docs/{...$rest}/end` for `/docs/guide/end`.
379+
328380
## Layout Routes
329381

330382
Layout routes are used to wrap child routes with additional components and logic. They are useful for:

docs/start/framework/react/migrate-from-next-js.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,15 @@ Now that you have migrated the basic structure of your Next.js application to Ta
338338

339339
### Routing Concepts
340340

341-
| Route Example | Next.js | TanStack Start |
342-
| ------------------------------ | ---------------------------------- | ------------------------- |
343-
| Root Layout | `src/app/layout.tsx` | `src/app/__root.tsx` |
344-
| `/` (Home Page) | `src/app/page.tsx` | `src/app/index.tsx` |
345-
| `/posts` (Static Route) | `src/app/posts/page.tsx` | `src/app/posts.tsx` |
346-
| `/posts/[slug]` (Dynamic) | `src/app/posts/[slug]/page.tsx` | `src/app/posts/$slug.tsx` |
347-
| `/posts/[...slug]` (Catch-All) | `src/app/posts/[...slug]/page.tsx` | `src/app/posts/$.tsx` |
348-
| `/api/endpoint` (API Route) | `src/app/api/endpoint/route.ts` | `src/app/api/endpoint.ts` |
341+
| Route Example | Next.js | TanStack Start |
342+
| ----------------------------------------- | ------------------------------------ | ------------------------------ |
343+
| Root Layout | `src/app/layout.tsx` | `src/app/__root.tsx` |
344+
| `/` (Home Page) | `src/app/page.tsx` | `src/app/index.tsx` |
345+
| `/posts` (Static Route) | `src/app/posts/page.tsx` | `src/app/posts.tsx` |
346+
| `/posts/[slug]` (Dynamic) | `src/app/posts/[slug]/page.tsx` | `src/app/posts/$slug.tsx` |
347+
| `/posts/[...slug]` (Catch-All) | `src/app/posts/[...slug]/page.tsx` | `src/app/posts/$.tsx` |
348+
| `/posts/[[...slug]]` (Optional Catch-All) | `src/app/posts/[[...slug]]/page.tsx` | `src/app/posts.{...$slug}.tsx` |
349+
| `/api/endpoint` (API Route) | `src/app/api/endpoint/route.ts` | `src/app/api/endpoint.ts` |
349350

350351
Learn more about the [Routing Concepts](/router/latest/docs/framework/react/routing/routing-concepts).
351352

0 commit comments

Comments
 (0)