dist/plugins/web-routing-tanstack-router/skills/web-routing-tanstack-router/SKILL.md
Type-safe client-side routing for React with file-based routes, search params validation, loaders, and code splitting
npx skillsauth add agents-inc/skills web-routing-tanstack-routerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: TanStack Router provides fully type-safe client-side routing for React. Use file-based routing with
@tanstack/router-pluginfor automatic route tree generation. Define search params with Zod via@tanstack/zod-adapter. Useloaderfor data fetching,beforeLoadfor guards/redirects, andcreateRootRouteWithContextfor dependency injection. Version: v1.x (stable).
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use createFileRoute for all file-based routes - NEVER define routes manually when using the router plugin)
(You MUST validate search params with validateSearch - NEVER read raw window.location.search)
(You MUST use beforeLoad for auth guards and redirects - NEVER check auth inside component render)
(You MUST pass services via router context - NEVER import them directly in loaders (breaks testability))
(You MUST use <Outlet /> in layout routes to render child content - forgetting it renders nothing)
</critical_requirements>
Auto-detection: TanStack Router, createFileRoute, createRootRoute, createRootRouteWithContext, createRouter, RouterProvider, Outlet, useNavigate, useSearch, useParams, useLoaderData, useRouteContext, routeTree.gen, tanstack/react-router, tanstack/router-plugin, validateSearch, zodValidator, beforeLoad, loader, notFound, redirect
When to use:
Key patterns covered:
createFileRoute, createRootRoute)Link, useNavigate, redirect)beforeLoad middlewareautoCodeSplittingWhen NOT to use:
For quick API reference (hooks, components, route options), see reference.md.
TanStack Router treats the URL as a first-class, fully-typed state manager. Every path parameter, search parameter, and loader return type is inferred through TypeScript, catching routing bugs at compile time rather than runtime. The router plugin generates a route tree from your file system, giving you type-safe <Link> components and useNavigate calls that validate destinations, params, and search params automatically.
Core principles:
@tanstack/router-plugincreateRootRouteWithContext, not global importsWhen to use TanStack Router:
When NOT to use:
Install @tanstack/react-router, @tanstack/router-plugin, and optionally @tanstack/zod-adapter. Configure the Vite plugin with autoCodeSplitting: true before the React plugin. Register the router via declare module for app-wide type safety.
// vite.config.ts - Router plugin MUST come before React plugin
tanstackRouter({ target: "react", autoCodeSplitting: true }),
react(),
// src/main.tsx - Register for type safety
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
See examples/core.md for complete setup with context and devtools.
The router plugin generates a typed route tree from your file structure. Key conventions:
| Convention | Example | Purpose |
| ------------ | -------------------- | ---------------------------------------- |
| __root.tsx | __root.tsx | Root layout, wraps entire app |
| index.tsx | posts/index.tsx | Index route for directory (/posts) |
| $param | $postId/index.tsx | Dynamic path parameter |
| _prefix | _authenticated.tsx | Pathless layout (no URL segment) |
| route.tsx | posts/route.tsx | Layout for directory children |
| suffix_ | posts_.detail.tsx | Non-nested route (escapes parent layout) |
| $ | $.tsx | Splat/catch-all route |
| -prefix | -components.tsx | Ignored by router (not a route) |
| (group) | (admin)/ | Organizational grouping (no URL effect) |
// src/routes/posts/index.tsx
export const Route = createFileRoute("/posts/")({
component: PostsIndex,
});
See examples/routes.md for nested layouts, pathless routes, non-nested routes, and catch-all routes.
TanStack Router validates all navigation destinations, params, and search params at compile time.
// Declarative - Link component
<Link to="/posts/$postId" params={{ postId: post.id }} preload="intent">
{post.title}
</Link>
// Imperative - after side effects
const navigate = useNavigate();
await navigate({ to: "/posts/$postId", params: { postId: post.id }, replace: true });
// In loaders/beforeLoad - redirect
throw redirect({ to: "/login", search: { redirect: location.href } });
See examples/navigation.md for active states, search param updaters, and navigation decision tree.
Validate and type search params with validateSearch. Use Zod adapter for schema-based validation with fallback() for safe defaults.
import { zodValidator, fallback } from "@tanstack/zod-adapter";
const DEFAULT_PAGE = 1;
const schema = z.object({
page: fallback(z.number().min(1), DEFAULT_PAGE).default(DEFAULT_PAGE),
q: fallback(z.string(), "").default(""),
});
export const Route = createFileRoute("/products/")({
validateSearch: zodValidator(schema),
component: ProductsPage,
});
// In component: fully typed
const { page, q } = Route.useSearch();
See examples/search-params.md for complete filter pages, search middleware, plain function validation, and declarative vs imperative updates.
Loaders fetch data before the component renders. They run in parallel for sibling routes and support SWR-style caching.
export const Route = createFileRoute("/posts/$postId/")({
staleTime: STALE_TIME_MS,
loader: async ({ params, context, abortController }) => {
const post = await context.apiClient.getPost(params.postId, {
signal: abortController.signal,
});
return { post };
},
component: PostDetail,
});
// Data is guaranteed available - no loading state needed
const { post } = Route.useLoaderData();
beforeLoad runs first (sequentially) for auth checks and context enrichment. loader runs after (in parallel with siblings) for data fetching.
See examples/data-loading.md for external data fetching integration, non-blocking prefetch, and SWR caching.
Layout routes wrap child routes with shared UI. Use <Outlet /> to render matched child content.
// src/routes/posts/route.tsx - Layout for /posts/*
export const Route = createFileRoute("/posts")({
component: () => (
<div className="posts-layout">
<aside>{/* Sidebar */}</aside>
<Outlet /> {/* Renders child route */}
</div>
),
});
Pathless layouts (_prefix) add UI/guards without affecting the URL:
// src/routes/_authenticated.tsx -> children at /dashboard, /settings (no /_authenticated in URL)
export const Route = createFileRoute("/_authenticated")({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) throw redirect({ to: "/login" });
},
component: () => <Outlet />,
});
See examples/routes.md for nested pathless layouts, non-nested routes, and catch-all routes.
Use createRootRouteWithContext to inject dependencies (auth, data clients, services) into all routes via typed context.
// Root: define context shape
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootLayout,
});
// Entry: provide context
const router = createRouter({
routeTree,
context: { auth: authService, apiClient },
});
// Routes: access typed context in loaders
loader: async ({ context }) => {
const posts = await context.apiClient.getPosts();
return { posts };
};
See examples/auth-and-context.md for complete patterns including context enrichment via beforeLoad and getRouteApi for shared components.
const PENDING_DELAY_MS = 200;
export const Route = createFileRoute("/posts/$postId/")({
pendingMs: PENDING_DELAY_MS,
pendingComponent: () => <div>Loading...</div>,
errorComponent: ({ error, reset }) => (
<div role="alert">
<pre>{error.message}</pre>
<button type="button" onClick={reset}>Retry</button>
</div>
),
notFoundComponent: () => <div>Post not found</div>,
loader: async ({ params }) => {
const post = await fetchPost(params.postId);
if (!post) throw notFound();
return { post };
},
component: PostDetail,
});
See examples/error-handling.md for router-level defaults, code splitting strategies, and preloading.
</patterns><decision_framework>
Need to navigate?
+-- Is it a clickable element in JSX?
| +-- YES -> Use <Link to="..." />
| +-- NO -> Is it after a side effect (form submit, mutation)?
| +-- YES -> Use useNavigate()
| +-- NO -> Is it in a loader/beforeLoad?
| +-- YES -> throw redirect()
| +-- NO -> Use router.navigate()
What does the logic do?
+-- Auth check / permission guard?
| -> beforeLoad (blocks everything, runs first)
+-- Redirect based on conditions?
| -> beforeLoad (throw redirect())
+-- Add data to context for children?
| -> beforeLoad (return value merges into context)
+-- Fetch data for the component?
| -> loader (runs in parallel with siblings)
+-- Prefetch data for an external cache?
| -> loader (prefetch via context-injected client, runs in parallel)
How to load data?
+-- Simple app, no shared cache needs?
| -> Built-in route loaders with staleTime
+-- Complex app with shared server state cache?
| -> External data fetching library + prefetch in loaders
+-- Data needed only for this component?
| -> Route loader (useLoaderData)
+-- Data shared across many components?
| -> External data fetching library (client in context)
Validating search params?
+-- Complex schema with many fields?
| -> Zod adapter (zodValidator + fallback)
+-- Simple 1-2 params?
| -> Plain validateSearch function
+-- Need shared schema with forms?
| -> Zod adapter (share schema between route and form)
+-- Zod 3.24.0+ / Zod 4+ with Standard Schema?
| -> Can use Zod directly without adapter (use `.catch()` for defaults)
Need shared UI across routes?
+-- Shared UI for a URL segment (/posts/*)?
| -> route.tsx in directory (posts/route.tsx)
+-- Shared UI without URL segment (auth guard)?
| -> Pathless layout (_authenticated.tsx)
+-- Route should escape parent layout?
| -> Non-nested route suffix (posts_.detail.tsx)
+-- Organizational grouping only?
| -> Group directory ((admin)/)
</decision_framework>
Package ecosystem:
| Package | Purpose |
| --------------------------------- | ------------------------------------------ |
| @tanstack/react-router | Core router for React |
| @tanstack/router-plugin | Vite/Webpack plugin for file-based routing |
| @tanstack/react-router-devtools | Development tools |
| @tanstack/zod-adapter | Zod integration for search params |
| @tanstack/valibot-adapter | Valibot integration for search params |
Schema validation adapters: Use @tanstack/zod-adapter or @tanstack/valibot-adapter for search params. Zod 3.24.0+ supports Standard Schema and can be used directly without an adapter.
External data fetching: The router context system (createRootRouteWithContext) enables injecting any data fetching client. Loaders can prefetch data via context, and components consume cached data. See examples/data-loading.md.
Conflicts with other client routers: TanStack Router replaces any other client-side routing solution. Do not combine with another router library.
</integration><red_flags>
High Priority Issues:
createRoute when using file-based routing plugin (routes will be out of sync with generated tree)window.location.search directly instead of useSearch() (bypasses validation, breaks SSR, loses reactivity)beforeLoad (component flashes before redirect, race conditions)<Outlet /> in layout routes (child routes render nothing)Medium Priority Issues:
fallback() with Zod adapter (invalid search params throw errors instead of falling back to defaults)declare module "@tanstack/react-router" registration (Link, navigate lose type safety across the app)staleTime: 0 when an external data cache handles freshness (double-fetching on every navigation)Common Mistakes:
beforeLoad instead of loader (creates waterfalls, beforeLoad runs sequentially)context when creating the router (TypeScript error if using createRootRouteWithContext)to="." without from in child components (path resolution may be wrong)redirect search param in the login page (users lose their intended destination)Gotchas and Edge Cases:
beforeLoad runs sequentially (parent before child) while loader runs in parallel - heavy work in beforeLoad creates waterfallsrouteTree.gen.ts file is auto-generated - never edit it manually, it will be overwrittencreateFileRoute path string must exactly match the file's location in the routes directorynotFound() and redirect() must be thrown, not returneduseSearch({ strict: false }) returns a partial type - only use when you genuinely do not know which route you are on$postId) are always strings - cast to number in the loader if needed, not in the component</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use createFileRoute for all file-based routes - NEVER define routes manually when using the router plugin)
(You MUST validate search params with validateSearch - NEVER read raw window.location.search)
(You MUST use beforeLoad for auth guards and redirects - NEVER check auth inside component render)
(You MUST pass services via router context - NEVER import them directly in loaders (breaks testability))
(You MUST use <Outlet /> in layout routes to render child content - forgetting it renders nothing)
Failure to follow these rules will break type safety, create security vulnerabilities in auth flows, and cause routes to silently render nothing.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety