tanstack-router
SKILL.md
TanStack Router Best Practices
Setup
npm install @tanstack/react-router
npm install -D @tanstack/router-plugin
Vite Plugin
// vite.config.ts
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
export default defineConfig({
plugins: [TanStackRouterVite(), react()],
});
The plugin generates a type-safe route tree from your file structure.
Router Registration
Register your router type globally for full type safety:
// src/router.ts
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
This enables type-safe Link, useNavigate, useParams, and useSearch across your entire app.
File-Based Routing
src/routes/
├── __root.tsx # Root layout (always rendered)
├── index.tsx # /
├── about.tsx # /about
├── users/
│ ├── index.tsx # /users
│ ├── $userId.tsx # /users/:userId (dynamic param)
│ └── $userId/
│ └── posts.tsx # /users/:userId/posts
├── _auth/ # Layout route group (no URL segment)
│ ├── route.tsx # Auth layout wrapper
│ ├── dashboard.tsx # /dashboard (wrapped in auth layout)
│ └── settings.tsx # /settings (wrapped in auth layout)
└── _auth.tsx # Layout route file
Naming Conventions
| Pattern | Purpose | Example |
|---|---|---|
index.tsx |
Index route | /users |
$param.tsx |
Dynamic parameter | /users/:userId |
_layout/ |
Layout group (no URL segment) | Auth wrapper |
_layout.tsx |
Layout route component | Layout with outlet |
$.tsx |
Splat / catch-all | /files/* |
(group)/ |
Pathless grouping (organization only) | Feature grouping |
Root Route
// src/routes/__root.tsx
import { createRootRoute, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: () => (
<>
<Header />
<main>
<Outlet />
</main>
<Footer />
</>
),
notFoundComponent: () => <NotFound />,
errorComponent: ({ error }) => <ErrorPage error={error} />,
});
Route Components
// src/routes/users/$userId.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/users/$userId")({
component: UserPage,
loader: async ({ params }) => {
return fetchUser(params.userId);
},
errorComponent: ({ error }) => <div>User not found</div>,
});
function UserPage() {
const { userId } = Route.useParams();
const user = Route.useLoaderData();
return <div>{user.name}</div>;
}
Search Params
Type-safe, validated search params using Zod or inline validators:
import { z } from "zod";
const userSearchSchema = z.object({
page: z.number().default(1),
sort: z.enum(["name", "date"]).default("name"),
filter: z.string().optional(),
});
export const Route = createFileRoute("/users/")({
validateSearch: userSearchSchema,
component: UsersPage,
});
function UsersPage() {
const { page, sort, filter } = Route.useSearch();
const navigate = Route.useNavigate();
function setPage(newPage: number) {
navigate({ search: (prev) => ({ ...prev, page: newPage }) });
}
return <UserList page={page} sort={sort} filter={filter} />;
}
Search params are validated, defaulted, and fully typed.
Data Loading
Route Loaders
export const Route = createFileRoute("/users/$userId")({
loader: async ({ params, context }) => {
const user = await context.queryClient.ensureQueryData(userQueryOptions(params.userId));
return { user };
},
});
With TanStack Query
Integrate with TanStack Query for caching and background updates:
import { queryOptions } from "@tanstack/react-query";
function userQueryOptions(userId: string) {
return queryOptions({
queryKey: ["users", userId],
queryFn: () => api.users.getById(userId),
});
}
export const Route = createFileRoute("/users/$userId")({
loader: ({ params, context }) => {
return context.queryClient.ensureQueryData(userQueryOptions(params.userId));
},
component: UserPage,
});
function UserPage() {
const { userId } = Route.useParams();
const { data: user } = useSuspenseQuery(userQueryOptions(userId));
return <div>{user.name}</div>;
}
Providing Context
Pass shared dependencies (query client, auth) via router context:
const router = createRouter({
routeTree,
context: {
queryClient,
auth: undefined!, // set at render time
},
});
// In root
function App() {
const auth = useAuth();
return <RouterProvider router={router} context={{ auth }} />;
}
Authenticated Routes
// src/routes/_auth.tsx
export const Route = createFileRoute("/_auth")({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: "/login",
search: { redirect: location.href },
});
}
},
component: () => <Outlet />,
});
All routes under _auth/ are protected. Unauthenticated users are redirected to login with a return URL.
Navigation
Type-Safe Links
import { Link } from "@tanstack/react-router";
<Link to="/users/$userId" params={{ userId: "123" }}>
View User
</Link>
<Link
to="/users"
search={{ page: 2, sort: "name" }}
activeProps={{ className: "active" }}
>
Users
</Link>
Programmatic Navigation
const navigate = useNavigate();
navigate({ to: "/users/$userId", params: { userId: "123" } });
navigate({ to: "/users", search: (prev) => ({ ...prev, page: 2 }) });
navigate({ to: "..", replace: true }); // relative navigation
Code Splitting
Lazy-load route components:
// src/routes/dashboard.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard")({
component: () => import("./dashboard-component").then((m) => m.Dashboard),
});
Or use the lazyRouteComponent helper:
import { lazyRouteComponent } from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard")({
component: lazyRouteComponent(() => import("./dashboard-component")),
});
Pending UI
Show loading states during navigation:
export const Route = createFileRoute("/users")({
loader: () => fetchUsers(),
pendingComponent: () => <UserListSkeleton />,
pendingMinMs: 200, // avoid flash for fast loads
pendingMs: 1000, // show pending after 1s
});
Not Found Handling
// Per-route
export const Route = createFileRoute("/users/$userId")({
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
if (!user) throw notFound();
return user;
},
notFoundComponent: () => <div>User not found</div>,
});
// Global fallback (in __root.tsx)
notFoundComponent: () => <NotFoundPage />,
Devtools
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
// In root layout
<TanStackRouterDevtools position="bottom-right" />;
Weekly Installs
2
Repository
grahamcrackers/skillsFirst Seen
Feb 28, 2026
Security Audits
Installed on
cline2
github-copilot2
codex2
kimi-cli2
gemini-cli2
cursor2