Chapter 16: TanStack Router Setup
TanStack Router is a type-safe, file-based router for React. Every route path, parameter, search parameter, and loader is fully typed. Navigate to a nonexistent route? Compile error. Wrong parameter type? Compile error.
Installation and Configuration​
We installed TanStack Router in Chapter 3. Here is the setup:
// vite.config.ts
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
export default defineConfig({
plugins: [
TanStackRouterVite({
routesDirectory: "./src/routes",
generatedRouteTree: "./src/route-tree.gen.ts",
}),
react(),
// ...
],
});
The Vite plugin watches your src/routes/ directory and generates route-tree.gen.ts automatically. This generated file contains the full type information for all routes.
Creating the Router​
// src/app/router.ts
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "@/route-tree.gen";
import { queryClient } from "@/shared/lib/query-client";
export const router = createRouter({
routeTree,
context: {
queryClient,
},
defaultPreload: "intent", // Preload routes on hover/focus
defaultPreloadStaleTime: 0,
});
// Register the router for type safety
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
The declare module block is critical — it registers your specific router instance with TanStack Router's type system, enabling full type inference throughout your app.
File-Based Routing​
Routes are defined by files in src/routes/. The file name determines the URL path:
src/routes/
├── __root.tsx → Root layout (wraps everything)
├── index.tsx → /
├── about.tsx → /about
├── _authenticated.tsx → Layout wrapper (no URL segment)
├── _authenticated/
│ ├── dashboard.tsx → /dashboard
│ ├── projects/
│ │ ├── index.tsx → /projects
│ │ └── $projectId.tsx → /projects/:projectId
│ └── settings/
│ ├── index.tsx → /settings
│ └── profile.tsx → /settings/profile
├── _public.tsx → Public layout wrapper
└── _public/
├── login.tsx → /login
└── register.tsx → /register
Naming Conventions​
| Pattern | Meaning |
|---|---|
index.tsx | Index route for the directory |
$param.tsx | Dynamic parameter (:param in URL) |
_layout.tsx | Layout route (no URL segment, wraps children) |
__root.tsx | Root layout (double underscore) |
$ | Catch-all / splat route |
Root Route​
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import type { QueryClient } from "@tanstack/react-query";
interface RouterContext {
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootLayout,
});
function RootLayout() {
return (
<>
<Outlet />
{import.meta.env.VITE_ENABLE_DEVTOOLS === "true" && (
<TanStackRouterDevtools position="bottom-right" />
)}
</>
);
}
Router Context​
The root route's context is available to all child routes. We pass the queryClient so that route loaders can prefetch data:
// In any route's loader:
loader: ({ context }) => {
// context.queryClient is fully typed
return context.queryClient.ensureQueryData(projectsQueryOptions());
},
Layout Routes​
Layout routes add shared UI (navigation, sidebar) without adding a URL segment:
// src/routes/_authenticated.tsx
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { AppLayout } from "@/shared/components/layout/app-layout";
export const Route = createFileRoute("/_authenticated")({
beforeLoad: async ({ context }) => {
// Redirect to login if not authenticated
const isAuthenticated = await checkAuth();
if (!isAuthenticated) {
throw redirect({ to: "/login", search: { redirect: location.pathname } });
}
},
component: AuthenticatedLayout,
});
function AuthenticatedLayout() {
return (
<AppLayout>
<Outlet /> {/* Child routes render here */}
</AppLayout>
);
}
All routes under _authenticated/ inherit the layout and the auth check.
Dynamic Routes​
// src/routes/_authenticated/projects/$projectId.tsx
import { createFileRoute } from "@tanstack/react-router";
import { ProjectDetail } from "@/features/projects";
import { projectQueryOptions } from "@/features/projects/hooks/use-projects";
export const Route = createFileRoute("/_authenticated/projects/$projectId")({
// Loader prefetches data before render
loader: ({ params, context }) =>
context.queryClient.ensureQueryData(
projectQueryOptions(params.projectId) // params.projectId is typed as string
),
// Component renders the data
component: ProjectDetailPage,
// Pending component while loader runs
pendingComponent: () => <ProjectDetailSkeleton />,
// Error component if loader fails
errorComponent: ({ error }) => <ProjectDetailError error={error} />,
});
function ProjectDetailPage() {
const { projectId } = Route.useParams();
// projectId is typed as string
return <ProjectDetail projectId={projectId} />;
}
Route Groups for Organization​
Use dot notation for organizing routes without nested directories:
src/routes/_authenticated/
├── settings.tsx → /settings layout
├── settings.profile.tsx → /settings/profile
├── settings.team.tsx → /settings/team
├── settings.billing.tsx → /settings/billing
└── settings.notifications.tsx → /settings/notifications
DevTools​
TanStack Router DevTools show:
- Current route and params
- Search parameters
- Route cache state
- Navigation history
- Loader states
Include them conditionally in development:
{import.meta.env.VITE_ENABLE_DEVTOOLS === "true" && (
<TanStackRouterDevtools position="bottom-right" />
)}
Summary​
- ✅ File-based routing generates fully-typed route trees automatically
- ✅ Root route with context shares
queryClientand other dependencies - ✅ Layout routes (
_prefix) add shared UI without URL segments - ✅ Dynamic routes (
$param) have typed parameters - ✅ Route loaders prefetch data with TanStack Query integration
- ✅ Auth guards in
beforeLoadredirect unauthenticated users - ✅ DevTools visualize route state during development