Use when creating or modifying routes, route guards, layout routes, or navigation with TanStack Router
File-based routing with TanStack Router — route naming, guards, params, layout routes, and navigation.
src/routes/
├── __root.tsx # Root layout (double underscore)
├── _protected.tsx # Layout route (single underscore, pathless)
├── _protected/
│ ├── index.tsx # URL: /
│ └── $organizationId.tsx # URL: /$organizationId (dynamic param)
├── _auth.tsx # Layout route for auth pages
├── _auth/
│ ├── login.tsx # URL: /login (not /_auth/login)
│ └── signup.tsx # URL: /signup
└── not-found.tsx # Standalone public route
Naming rules:
__root.tsx (double underscore)_ prefix (pathless — no URL segment)$paramNameindex.tsx within folderUse beforeLoad for authentication and authorization — never component-level checks.
export const Route = createFileRoute("/_protected")({
beforeLoad: async ({ location }) => {
// Authentication check — see your auth skill for implementation
const session = await getSession();
if (!session) {
// Preserve redirect path for post-login UX
const redirectPath = `${location.pathname}${location.search ? `?${new URLSearchParams(location.search).toString()}` : ""}`;
throw redirect({ to: "/login", search: { redirect: redirectPath } });
}
// Authorization checks (membership, role, etc.) — see your auth skill
},
component: ProtectedLayout,
});
export const Route = createFileRoute("/_protected/$organizationId")({
beforeLoad: async ({ params }) => {
const { organizationId } = params;
// Verify membership — see your auth skill
const membership = await checkMembership(organizationId);
if (!membership) throw redirect({ to: "/access-denied" });
},
component: Page_Dashboard,
});
ALWAYS use the from option for type safety:
// ✅ Full type safety
const { organizationId } = useParams({
from: "/_protected/$organizationId",
});
// ❌ Loses type safety
const { organizationId } = useParams({ strict: false }) as { organizationId: string };
const LoginSearchSchema = z.object({
redirect: z.string().optional().catch(undefined),
});
export const Route = createFileRoute("/_auth/login")({
validateSearch: LoginSearchSchema.parse,
beforeLoad: async ({ search }) => {
const session = await getSession();
if (session) throw redirect({ to: search.redirect || "/" });
},
component: Page_Login,
});
Layout routes wrap child routes with shared UI (nav, sidebar, theme). They use the _ prefix convention and render children via <Outlet />.
CRITICAL: Never use wrapper components for shared layouts. If 2+ routes share a visual frame, it MUST be a layout route.
// src/routes/_auth.tsx
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/_auth")({
component: AuthLayout,
});
function AuthLayout() {
return (
<div style={{ /* shared layout styles */ }}>
{/* Shared UI: sidebars, navs, decorations */}
<Outlet /> {/* Child route renders here */}
</div>
);
}
Child routes become pure content — no layout wrapping.
Create when: 2+ routes share a visual wrapper, routes need a common guard, or pages need a shared container.
Don't create when: Only one page uses it (inline in page) or it's just a provider (put in __root.tsx).
Layout routes define the container size; child pages use relative heights:
// Layout provides sized container
function ProtectedLayout() {
return (
<div style={{ height: "calc(100vh - navHeight)" }}>
<Outlet />
</div>
);
}
// Page uses height: 100% — NEVER recalculate viewport
function Page_Dashboard() {
return <div style={{ height: "100%" }}>{/* content */}</div>;
}
After creating or moving route files, the route tree auto-regenerates during dev server. The generated routeTree.gen.ts is auto-managed — never edit manually.
| Wrong | Correct |
|---|---|
_root.tsx (single underscore) | __root.tsx (double) |
:paramName or {paramName} | $paramName |
| Component-level auth checks | beforeLoad guard |
| Cache-based security decisions | Direct database query in guard |
| No redirect path preservation | Pass location.pathname as search param |
useParams({ strict: false }) | useParams({ from: '/path/$param' }) |
| Wrapper components for shared layouts | Layout routes with <Outlet /> |
Layout without <Outlet /> | Always include <Outlet /> for child rendering |
createFileRoute("/login") for _auth/login.tsx | createFileRoute("/_auth/login") |
None — TanStack Router file-based routing is the standard.
Install: pnpm add @tanstack/react-router
Create root route:
src/routes/__root.tsximport { createRootRoute, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: () => <Outlet />,
});