{error.message}
+ +Are you sure you want to leave?
+ + +Not found!
+ Go home +Post "{postId}" not found
+ }, +}) + +function PostComponent() { + const { post } = Route.useLoaderData() + returnError: {error.message}
+ +Something went wrong: {error.message}
+ +Item {params.id} not found
+} +``` + +To forward partial data, use the `data` option on `notFound()`: + +```tsx +loader: async ({ params }) => { + const partialData = await getPartialData(params.id) + if (!partialData.fullResource) { + throw notFound({ data: { name: partialData.name } }) + } + return partialData +}, +notFoundComponent: ({ data }) => { + // data is typed as unknown — validate it + const info = data as { name: string } | undefined + return{info?.name ?? 'Resource'} not found
+}, +``` + +## Route Masking + +Route masking shows a different URL in the browser bar than the actual route being rendered. Masking data is stored in `location.state` and is lost when the URL is shared or opened in a new tab. + +### Imperative Masking on `` + +```tsx +import { Link } from '@tanstack/react-router' + +function PhotoGrid({ photoId }: { photoId: string }) { + return ( + + Open Photo + + ) +} +``` + +### Imperative Masking with `useNavigate` + +```tsx +import { useNavigate } from '@tanstack/react-router' + +function OpenPhotoButton({ photoId }: { photoId: string }) { + const navigate = useNavigate() + + return ( + + ) +} +``` + +### Declarative Masking with `createRouteMask` + +```tsx +import { createRouter, createRouteMask } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const photoModalMask = createRouteMask({ + routeTree, + from: '/photos/$photoId/modal', + to: '/photos/$photoId', + params: (prev) => ({ photoId: prev.photoId }), +}) + +const router = createRouter({ + routeTree, + routeMasks: [photoModalMask], +}) +``` + +### Unmasking on Reload + +By default, masks survive local page reloads. To unmask on reload: + +```tsx +// Per-mask +const mask = createRouteMask({ + routeTree, + from: '/photos/$photoId/modal', + to: '/photos/$photoId', + params: (prev) => ({ photoId: prev.photoId }), + unmaskOnReload: true, +}) + +// Per-link + + Open Photo + + +// Router-wide default +const router = createRouter({ + routeTree, + unmaskOnReload: true, +}) +``` + +## Common Mistakes + +### 1. HIGH: Using deprecated `NotFoundRoute` + +```tsx +// WRONG — NotFoundRoute blocks notFound() and notFoundComponent from working +import { NotFoundRoute } from '@tanstack/react-router' +const notFoundRoute = new NotFoundRoute({ component: () =>404
}) +const router = createRouter({ routeTree, notFoundRoute }) + +// CORRECT — use notFoundComponent on root route +export const Route = createRootRoute({ + component: () =>404
, +}) +``` + +### 2. MEDIUM: Expecting `useLoaderData` in `notFoundComponent` + +```tsx +// WRONG — loader may not have completed +notFoundComponent: () => { + const data = Route.useLoaderData() // may be undefined! + return{data.title} not found
+} + +// CORRECT — use safe hooks +notFoundComponent: () => { + const { postId } = Route.useParams() + returnPost {postId} not found
+} +``` + +### 3. MEDIUM: Leaf routes cannot handle not-found errors + +Only routes with children (and therefore an `Not found
, +}) +``` + +### 4. MEDIUM: Expecting masked URLs to survive sharing + +Masking data lives in `location.state` (browser history). When a masked URL is copied, shared, or opened in a new tab, the masking data is lost. The browser navigates to the visible (masked) URL directly. + +### 5. HIGH (cross-skill): Using `reset()` alone instead of `router.invalidate()` + +```tsx +// WRONG — reset() clears the error boundary but does NOT re-run the loader +function ErrorFallback({ error, reset }: { error: Error; reset: () => void }) { + return +} + +// CORRECT — invalidate re-runs loaders and resets the error boundary +function ErrorFallback({ error }: { error: Error; reset: () => void }) { + const router = useRouter() + return ( + + ) +} +``` + +## Cross-References + +- **router-core/data-loading** — `notFound()` thrown in loaders interacts with error boundaries and loader data availability. `errorComponent` retry requires `router.invalidate()`. +- **router-core/type-safety** — `notFoundComponent` data is typed as `unknown`; validate before use. diff --git a/packages/router-core/skills/router-core/path-params/SKILL.md b/packages/router-core/skills/router-core/path-params/SKILL.md new file mode 100644 index 00000000000..32964d06ccc --- /dev/null +++ b/packages/router-core/skills/router-core/path-params/SKILL.md @@ -0,0 +1,382 @@ +--- +name: router-core/path-params +description: >- + Dynamic path segments ($paramName), splat routes ($ / _splat), + optional params ({-$paramName}), prefix/suffix patterns ({$param}.ext), + useParams, params.parse/stringify, pathParamsAllowedCharacters, + i18n locale patterns. +type: sub-skill +library: tanstack-router +library_version: '1.166.2' +requires: + - router-core +sources: + - TanStack/router:docs/router/guide/path-params.md + - TanStack/router:docs/router/routing/routing-concepts.md + - TanStack/router:docs/router/guide/internationalization-i18n.md +--- + +# Path Params + +Path params capture dynamic URL segments into named variables. They are defined with a `$` prefix in the route path. + +> **CRITICAL**: Never interpolate params into the `to` string. Always use the `params` prop. This is the most common agent mistake for path params. + +> **CRITICAL**: Types are fully inferred. Never annotate the return of `useParams()`. + +## Dynamic Segments + +A segment prefixed with `$` captures text until the next `/`. + +```tsx +// src/routes/posts.$postId.tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params }) => { + // params.postId is string — fully inferred, do not annotate + return fetchPost(params.postId) + }, + component: PostComponent, +}) + +function PostComponent() { + const { postId } = Route.useParams() + const data = Route.useLoaderData() + return ( ++ Page {page}, filter: {filter}, sort: {sort} +
+