tanstack-router
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" />;
More from grahamcrackers/skills
react-aria-components
React Aria Components patterns for building accessible, unstyled UI with composition-based architecture. Covers component structure, styling with Tailwind and CSS, render props, collections, forms, selections, overlays, and drag-and-drop. Use when building accessible components, using react-aria-components, creating design systems, or when the user asks about React Aria, accessible UI primitives, or headless component libraries.
15vitest-testing
Vitest and Testing Library patterns for unit, component, and integration tests in TypeScript and React projects. Covers test organization, mocking, assertions, component testing, and MSW integration. Use when writing tests, setting up Vitest, mocking dependencies, testing React components, or when the user asks about testing patterns or test configuration.
3