Agent skill
tanstack-router
Use when setting up routes, implementing navigation, or configuring route loaders. TanStack Router patterns for type-safe, file-based routing. Covers installation, route configuration, typed params/search, layouts, and navigation.
Install this agent skill to your Project
npx add-skill https://github.com/MadAppGang/claude-code/tree/main/plugins/frontend/skills/tanstack-router
SKILL.md
TanStack Router Patterns
Type-safe, file-based routing for React applications with TanStack Router.
Installation
pnpm add @tanstack/react-router
pnpm add -D @tanstack/router-plugin
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react(),
TanStackRouterVite(), // Generates route tree
],
})
Bootstrap
// src/main.tsx
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({ routeTree })
// Register router for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
)
File-Based Routes
src/routes/
├── __root.tsx # Root layout (Outlet, providers)
├── index.tsx # "/" route
├── about.tsx # "/about" route
├── users/
│ ├── index.tsx # "/users" route
│ └── $userId.tsx # "/users/:userId" route (dynamic)
└── posts/
├── $postId/
│ ├── index.tsx # "/posts/:postId" route
│ └── edit.tsx # "/posts/:postId/edit" route
└── index.tsx # "/posts" route
Naming Conventions:
__root.tsx- Root layout (contains<Outlet />)index.tsx- Index route for that path$param.tsx- Dynamic parameter (e.g.,$userId→:userId)_layout.tsx- Layout route (no URL segment)route.lazy.tsx- Lazy-loaded route
Root Layout
// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users">Users</Link>
</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
<TanStackRouterDevtools /> {/* Auto-hides in production */}
</>
),
})
Basic Route
// src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: AboutComponent,
})
function AboutComponent() {
return <div>About Page</div>
}
Dynamic Routes with Params
// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$userId')({
component: UserComponent,
})
function UserComponent() {
const { userId } = Route.useParams() // Fully typed!
return <div>User ID: {userId}</div>
}
Typed Search Params
// src/routes/users/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const userSearchSchema = z.object({
page: z.number().default(1),
filter: z.enum(['active', 'inactive', 'all']).default('all'),
search: z.string().optional(),
})
export const Route = createFileRoute('/users/')({
validateSearch: userSearchSchema,
component: UsersComponent,
})
function UsersComponent() {
const { page, filter, search } = Route.useSearch() // Fully typed!
return (
<div>
<p>Page: {page}</p>
<p>Filter: {filter}</p>
{search && <p>Search: {search}</p>}
</div>
)
}
Navigation with Link
import { Link } from '@tanstack/react-router'
// Basic navigation
<Link to="/about">About</Link>
// With params
<Link to="/users/$userId" params={{ userId: '123' }}>
View User
</Link>
// With search params
<Link
to="/users"
search={{ page: 2, filter: 'active' }}
>
Users Page 2
</Link>
// With state
<Link to="/details" state={{ from: 'home' }}>
Details
</Link>
// Active link styling
<Link
to="/about"
activeProps={{ className: 'text-blue-600 font-bold' }}
inactiveProps={{ className: 'text-gray-600' }}
>
About
</Link>
Programmatic Navigation
import { useNavigate } from '@tanstack/react-router'
function MyComponent() {
const navigate = useNavigate()
const handleClick = () => {
// Navigate to route
navigate({ to: '/users' })
// With params
navigate({ to: '/users/$userId', params: { userId: '123' } })
// With search
navigate({ to: '/users', search: { page: 2 } })
// Replace history
navigate({ to: '/login', replace: true })
// Go back
navigate({ to: '..' }) // Relative navigation
}
return <button onClick={handleClick}>Navigate</button>
}
Route Loaders (Data Fetching)
Basic Loader:
// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$userId')({
loader: async ({ params }) => {
const user = await fetchUser(params.userId)
return { user }
},
component: UserComponent,
})
function UserComponent() {
const { user } = Route.useLoaderData() // Fully typed!
return <div>{user.name}</div>
}
With TanStack Query Integration (see router-query-integration skill for details):
import { queryClient } from '@/app/queryClient'
import { userQuery Options } from '@/features/users/queries'
export const Route = createFileRoute('/users/$userId')({
loader: ({ params }) =>
queryClient.ensureQueryData(userQueryOptions(params.userId)),
component: UserComponent,
})
Layouts
Layout Route (_layout.tsx - no URL segment):
// src/routes/_layout.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_layout')({
component: LayoutComponent,
})
function LayoutComponent() {
return (
<div className="dashboard-layout">
<Sidebar />
<div className="content">
<Outlet /> {/* Child routes */}
</div>
</div>
)
}
// Child routes
// src/routes/_layout/dashboard.tsx → "/dashboard"
// src/routes/_layout/settings.tsx → "/settings"
Loading States
export const Route = createFileRoute('/users')({
loader: async () => {
const users = await fetchUsers()
return { users }
},
pendingComponent: () => <Spinner />,
errorComponent: ({ error }) => <ErrorMessage>{error.message}</ErrorMessage>,
component: UsersComponent,
})
Error Handling
import { ErrorComponent } from '@tanstack/react-router'
export const Route = createFileRoute('/users')({
loader: async () => {
const users = await fetchUsers()
if (!users) throw new Error('Failed to load users')
return { users }
},
errorComponent: ({ error, reset }) => (
<div>
<h1>Error loading users</h1>
<p>{error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
),
component: UsersComponent,
})
Route Context
Providing Context:
// src/routes/__root.tsx
export const Route = createRootRoute({
beforeLoad: () => ({
user: getCurrentUser(),
}),
component: RootComponent,
})
// Access in child routes
export const Route = createFileRoute('/dashboard')({
component: function Dashboard() {
const { user } = Route.useRouteContext()
return <div>Welcome, {user.name}</div>
},
})
Route Guards / Auth
// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context }) => {
if (!context.user) {
throw redirect({ to: '/login' })
}
},
component: Outlet,
})
// Protected routes
// src/routes/_authenticated/dashboard.tsx
// src/routes/_authenticated/profile.tsx
Preloading
Hover Preload:
<Link
to="/users/$userId"
params={{ userId: '123' }}
preload="intent" // Preload on hover
>
View User
</Link>
Options:
preload="intent"- Preload on hover/focuspreload="render"- Preload when link renderspreload={false}- No preload (default)
DevTools
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
// Add to root layout
<TanStackRouterDevtools position="bottom-right" />
Auto-hides in production builds.
Best Practices
- Use Type-Safe Navigation - Let TypeScript catch routing errors at compile time
- Validate Search Params - Use Zod schemas for search params
- Prefetch Data in Loaders - Integrate with TanStack Query for optimal data fetching
- Use Layouts for Shared UI - Avoid duplicating layout code across routes
- Lazy Load Routes - Use
route.lazy.tsxfor code splitting - Leverage Route Context - Share data down the route tree efficiently
Common Patterns
Catch-All Route:
// src/routes/$.tsx
export const Route = createFileRoute('/$')({
component: () => <div>404 Not Found</div>,
})
Optional Params:
// Use search params for optional data
const searchSchema = z.object({
optional: z.string().optional(),
})
Multi-Level Dynamic Routes:
/posts/$postId/comments/$commentId
Related Skills
- tanstack-query - Data fetching and caching
- router-query-integration - Integrating Router loaders with Query
- core-principles - Project structure with routes
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
test-skill
A test skill for validation testing. Use when testing skill parsing and validation logic.
bad-skill
claudish-usage
CRITICAL - Guide for using Claudish CLI ONLY through sub-agents to run Claude Code with OpenRouter models (Grok, GPT-5, Gemini, MiniMax). NEVER run Claudish directly in main context unless user explicitly requests it. Use when user mentions external AI models, Claudish, OpenRouter, or alternative models. Includes mandatory sub-agent delegation patterns, agent selection guide, file-based instructions, and strict rules to prevent context window pollution.
release
Plugin release process for MAG Claude Plugins marketplace. Covers version bumping, marketplace.json updates, git tagging, and common mistakes. Use when releasing new plugin versions or troubleshooting update issues.
claudish-integration
openrouter-trending-models
Fetch trending programming models from OpenRouter rankings. Use when selecting models for multi-model review, updating model recommendations, or researching current AI coding trends. Provides model IDs, context windows, pricing, and usage statistics from the most recent week.
Didn't find tool you were looking for?