Skip to main content

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​

PatternMeaning
index.tsxIndex route for the directory
$param.tsxDynamic parameter (:param in URL)
_layout.tsxLayout route (no URL segment, wraps children)
__root.tsxRoot 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 queryClient and 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 beforeLoad redirect unauthenticated users
  • ✅ DevTools visualize route state during development