Skip to content

Commit eed4b0b

Browse files
committed
docs
1 parent cac7a3b commit eed4b0b

File tree

2 files changed

+318
-0
lines changed

2 files changed

+318
-0
lines changed

docs/router/framework/react/api/router/RouteOptionsType.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,27 @@ The `RouteOptions` type accepts an object with the following properties:
8888
- Type: `(params: TParams) => Record<string, string>`
8989
- A function that will be called when this route's parsed params are being used to build a location. This function should return a valid object of `Record<string, string>` mapping.
9090

91+
### `skipRouteOnParseError` property (⚠️ experimental)
92+
93+
> [!WARNING]
94+
> The `skipRouteOnParseError` option is currently **experimental** and may change in future releases.
95+
96+
- Type:
97+
98+
```tsx
99+
type skipRouteOnParseError = {
100+
params?: boolean
101+
priority?: number
102+
}
103+
```
104+
105+
- Optional
106+
- By default, when a route's `params.parse` function throws an error, the route will match and then show an error state during render. With `skipRouteOnParseError.params` enabled, the router will skip routes whose `params.parse` function throws and continue searching for alternative matching routes.
107+
- See [Guides > Path Params > Validating path parameters during matching](../../guide/path-params#validating-path-parameters-during-matching) for detailed usage examples.
108+
109+
> [!IMPORTANT]
110+
> **Performance impact**: This option has a **non-negligible performance cost** and should not be used indiscriminately. Routes with `skipRouteOnParseError` are placed on separate branches in the route matching tree instead of sharing nodes with other dynamic routes. This reduces the tree's ability to efficiently narrow down matches and requires testing more route, even for routes that wouldn't match the path structure alone.
111+
91112
### `beforeLoad` method
92113
93114
- Type:

docs/router/framework/react/guide/path-params.md

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,303 @@ function ShopComponent() {
738738

739739
Optional path parameters provide a powerful and flexible foundation for implementing internationalization in your TanStack Router applications. Whether you prefer prefix-based or combined approaches, you can create clean, SEO-friendly URLs while maintaining excellent developer experience and type safety.
740740

741+
## Validating Path Parameters During Matching
742+
743+
> [!WARNING]
744+
> The `skipRouteOnParseError` option is currently **experimental** and may change in future releases.
745+
746+
> [!IMPORTANT]
747+
> **Performance cost**: This feature has a **non-negligible performance cost** and should not be used indiscriminately. It creates additional branches in the route matching tree, reducing matching efficiency and requiring more route evaluations. Use it only when you genuinely need type-specific routes at the same path level.
748+
749+
By default, TanStack Router matches routes based purely on URL structure. A route with `/$param` will match any value for that parameter, and validation via `params.parse` happens later in the route lifecycle. If validation fails, the route shows an error state.
750+
751+
However, sometimes you want routes to match only when parameters meet specific criteria, with the router automatically falling back to alternative routes when validation fails. This is where `skipRouteOnParseError` comes in.
752+
753+
### When to Use This Feature
754+
755+
Use `skipRouteOnParseError.params` when you need:
756+
757+
- **Type-specific routes**: Different routes for UUIDs vs. slugs at the same path (e.g., `/$uuid` and `/$slug`)
758+
- **Format-specific routes**: Date-formatted paths vs. regular slugs (e.g., `/posts/2024-01-15` vs. `/posts/my-post`)
759+
- **Numeric vs. string routes**: Different behavior for numeric IDs vs. usernames (e.g., `/users/123` vs. `/users/johndoe`)
760+
- **Pattern-based routing**: Complex validation patterns where you want automatic fallback to other routes
761+
762+
Before using `skipRouteOnParseError.params`, consider whether you can achieve your goals with standard route matching:
763+
764+
- Using a static route prefix (e.g., `/id/$id` vs. `/username/$username`)
765+
- Using a prefix or suffix in the path (e.g., `/user-{$id}` vs. `/$username`)
766+
767+
### How It Works
768+
769+
The `skipRouteOnParseError` option changes when `params.parse` runs, but not what it does:
770+
771+
**Without `skipRouteOnParseError`**:
772+
773+
- Route matches based on URL structure alone
774+
- `params.parse` runs during route lifecycle (after match)
775+
- Errors from `params.parse` cause error state, showing `errorComponent`
776+
777+
**With `skipRouteOnParseError.params`**:
778+
779+
- Route matching includes running `params.parse`
780+
- Errors from `params.parse` cause route to be skipped, continuing to find other matches
781+
- If no route matches, shows `notFoundComponent`
782+
783+
Both modes still use `params.parse` for validation and transformation—the difference is timing and error handling.
784+
785+
### Basic Example: Numeric IDs with String Fallback
786+
787+
```tsx
788+
// routes/$id.tsx - Only matches numeric IDs
789+
export const Route = createFileRoute('/$id')({
790+
params: {
791+
parse: (params) => {
792+
const id = parseInt(params.id, 10)
793+
if (isNaN(id)) throw new Error('ID must be numeric')
794+
return { id }
795+
},
796+
},
797+
skipRouteOnParseError: { params: true },
798+
component: UserByIdComponent,
799+
})
800+
801+
function UserByIdComponent() {
802+
const { id } = Route.useParams() // id is number (from parsed params)
803+
const loaderData = Route.useLoaderData()
804+
return <div>User ID: {id}</div>
805+
}
806+
807+
// routes/$username.tsx - Matches any string
808+
export const UsernameRoute = createFileRoute('/$username')({
809+
// No params.parse - accepts any string
810+
component: UserByUsernameComponent,
811+
})
812+
813+
function UserByUsernameComponent() {
814+
const { username } = Route.useParams() // username is string
815+
return <div>Username: {username}</div>
816+
}
817+
```
818+
819+
With this setup:
820+
821+
- `/123` → Matches `/$id` route (validation passes)
822+
- `/johndoe` → Skips `/$id` (validation fails), matches `/$username` route
823+
824+
### Pattern-Based Validation Examples
825+
826+
#### UUID vs. Slug Routes
827+
828+
```tsx
829+
// routes/$uuid.tsx - Only matches valid UUIDs
830+
export const Route = createFileRoute('/$uuid')({
831+
params: {
832+
parse: (params) => {
833+
const uuidRegex =
834+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
835+
if (!uuidRegex.test(params.uuid)) {
836+
throw new Error('Not a valid UUID')
837+
}
838+
return { uuid: params.uuid }
839+
},
840+
},
841+
skipRouteOnParseError: { params: true },
842+
loader: async ({ params }) => fetchByUuid(params.uuid),
843+
component: UuidResourceComponent,
844+
})
845+
846+
// routes/$slug.tsx - Matches any string
847+
export const SlugRoute = createFileRoute('/$slug')({
848+
loader: async ({ params }) => fetchBySlug(params.slug),
849+
component: SlugResourceComponent,
850+
})
851+
```
852+
853+
Results:
854+
855+
- `/550e8400-e29b-41d4-a716-446655440000` → Matches UUID route
856+
- `/my-blog-post` → Matches slug route
857+
858+
#### Date-Formatted Posts
859+
860+
```tsx
861+
// routes/posts/$date.tsx - Only matches YYYY-MM-DD format
862+
export const Route = createFileRoute('/posts/$date')({
863+
params: {
864+
parse: (params) => {
865+
const date = new Date(params.date)
866+
if (isNaN(date.getTime())) {
867+
throw new Error('Invalid date format')
868+
}
869+
return { date }
870+
},
871+
},
872+
skipRouteOnParseError: { params: true },
873+
loader: async ({ params }) => fetchPostsByDate(params.date),
874+
component: DatePostsComponent,
875+
})
876+
877+
// routes/posts/$slug.tsx - Matches any string
878+
export const PostSlugRoute = createFileRoute('/posts/$slug')({
879+
loader: async ({ params }) => fetchPostBySlug(params.slug),
880+
component: PostComponent,
881+
})
882+
```
883+
884+
Results:
885+
886+
- `/posts/2024-01-15` → Matches date route, `params.date` is a Date object
887+
- `/posts/my-first-post` → Matches slug route
888+
889+
### Understanding Route Priority
890+
891+
When multiple routes could match the same URL, TanStack Router uses this priority order:
892+
893+
1. **Static routes** (highest priority) - e.g., `/settings`
894+
2. **Dynamic routes** - e.g., `/$slug`
895+
3. **Optional routes** - e.g., `/{-$lang}`
896+
4. **Wildcard routes** (lowest priority) - e.g., `/$`
897+
898+
When `skipRouteOnParseError` is used, validated routes are treated as having higher priority than non-validated routes _of the same category_. For example, a validated optional route has higher priority than a non-validated optional route, but lower priority than any dynamic route.
899+
900+
Example demonstrating priority:
901+
902+
```tsx
903+
// Static route - always matches /settings first
904+
export const SettingsRoute = createFileRoute('/settings')({
905+
component: SettingsComponent,
906+
})
907+
908+
// Validated route - matches numeric IDs
909+
export const IdRoute = createFileRoute('/$id')({
910+
params: {
911+
parse: (params) => ({ id: parseInt(params.id, 10) }),
912+
},
913+
skipRouteOnParseError: { params: true },
914+
component: IdComponent,
915+
})
916+
917+
// Non-validated route - fallback for any string
918+
export const SlugRoute = createFileRoute('/$slug')({
919+
component: SlugComponent,
920+
})
921+
```
922+
923+
Matching results:
924+
925+
- `/settings` → Static route (highest priority, even though ID route would validate)
926+
- `/123` → Validated dynamic route (`/$id` validation passes)
927+
- `/hello` → Non-validated dynamic route (`/$id` validation fails)
928+
929+
### Custom Priority Between Validated Routes
930+
931+
When you have multiple validated routes at the same level, and because `params.parse` can be any arbitrary code, you may have situations where multiple routes could potentially validate successfully. In these cases you can provide a custom priority as a tie-breaker using `skipRouteOnParseError.priority`.
932+
933+
Higher numbers mean higher priority, and no priority defaults to 0.
934+
935+
```tsx
936+
// routes/$uuid.tsx
937+
export const UuidRoute = createFileRoute('/$uuid')({
938+
params: {
939+
parse: (params) => {
940+
if (!isUuid(params.uuid)) throw new Error('Not a UUID')
941+
return params
942+
},
943+
},
944+
skipRouteOnParseError: {
945+
params: true,
946+
priority: 10, // Try this first
947+
},
948+
component: UuidComponent,
949+
})
950+
951+
// routes/$number.tsx
952+
export const NumberRoute = createFileRoute('/$number')({
953+
params: {
954+
parse: (params) => ({
955+
number: parseInt(params.number, 10),
956+
}),
957+
},
958+
skipRouteOnParseError: {
959+
params: true,
960+
priority: 5, // Try this second
961+
},
962+
component: NumberComponent,
963+
})
964+
965+
// routes/$slug.tsx
966+
export const SlugRoute = createFileRoute('/$slug')({
967+
// No validation - lowest priority by default
968+
component: SlugComponent,
969+
})
970+
```
971+
972+
Matching order:
973+
974+
1. Check UUID validation (priority 10)
975+
2. Check number validation (priority 5)
976+
3. Fall back to slug route (no validation)
977+
978+
### Nested Routes with Validation
979+
980+
Parent route validation gates access to child routes:
981+
982+
```tsx
983+
// routes/$orgId.tsx - Parent route, only matches numeric org IDs
984+
export const OrgRoute = createFileRoute('/$orgId')({
985+
params: {
986+
parse: (params) => ({
987+
orgId: parseInt(params.orgId, 10),
988+
}),
989+
},
990+
skipRouteOnParseError: { params: true },
991+
component: OrgLayoutComponent,
992+
})
993+
994+
// routes/$orgId/settings.tsx - Child route
995+
export const OrgSettingsRoute = createFileRoute('/$orgId/settings')({
996+
component: OrgSettingsComponent,
997+
})
998+
999+
// routes/$slug/settings.tsx - Alternative route
1000+
export const SlugSettingsRoute = createFileRoute('/$slug/settings')({
1001+
component: SettingsComponent,
1002+
})
1003+
```
1004+
1005+
Results:
1006+
1007+
- `/123/settings` → Matches `/$orgId/settings` (parent validation passes)
1008+
- `/my-org/settings` → Matches `/$slug/settings` (`/$orgId` validation fails)
1009+
1010+
### Working with Optional Parameters
1011+
1012+
`skipRouteOnParseError` works with optional parameters:
1013+
1014+
```tsx
1015+
// routes/{-$lang}/home.tsx - Validates language codes
1016+
export const Route = createFileRoute('/{-$lang}/home')({
1017+
params: {
1018+
parse: (params) => {
1019+
const validLangs = ['en', 'fr', 'es', 'de']
1020+
if (params.lang && !validLangs.includes(params.lang)) {
1021+
throw new Error('Invalid language code')
1022+
}
1023+
return { lang: params.lang || 'en' }
1024+
},
1025+
},
1026+
skipRouteOnParseError: { params: true },
1027+
component: HomeComponent,
1028+
})
1029+
```
1030+
1031+
Results:
1032+
1033+
- `/home` → Matches (optional param skipped, defaults to 'en')
1034+
- `/en/home` → Matches (validation passes)
1035+
- `/fr/home` → Matches (validation passes)
1036+
- `/it/home` → No match (validation fails, 'it' not in valid list)
1037+
7411038
## Allowed Characters
7421039

7431040
By default, path params are escaped with `encodeURIComponent`. If you want to allow other valid URI characters (e.g. `@` or `+`), you can specify that in your [RouterOptions](../api/router/RouterOptionsType.md#pathparamsallowedcharacters-property).

0 commit comments

Comments
 (0)