Chapter 17: Type-Safe Navigation
TanStack Router's type safety means navigation errors are caught at compile time, not in production. This chapter covers the Link component, useNavigate hook, and patterns for type-safe navigation.
The Link Component
import { Link } from "@tanstack/react-router";
// ✅ Type-safe — TypeScript verifies the route exists
<Link to="/projects">All Projects</Link>
// ✅ With typed params — $projectId is required
<Link to="/projects/$projectId" params={{ projectId: "proj-123" }}>
View Project
</Link>
// ✅ With typed search params
<Link
to="/projects"
search={{ status: "active", page: 1 }}
>
Active Projects
</Link>
// ❌ Compile error — route doesn't exist
<Link to="/nonexistent">Nowhere</Link>
// ❌ Compile error — missing required param
<Link to="/projects/$projectId">View Project</Link>
// ❌ Compile error — wrong param type
<Link to="/projects/$projectId" params={{ projectId: 123 }}>
View Project
</Link>
Active Link Styling
<Link
to="/projects"
className="text-muted-foreground hover:text-foreground transition-colors"
activeProps={{
className: "text-foreground font-semibold",
}}
>
Projects
</Link>
// Or with a render function for full control
<Link to="/projects">
{({ isActive }) => (
<span className={cn(
"flex items-center gap-2 px-3 py-2 rounded-md text-sm",
isActive
? "bg-accent text-accent-foreground font-medium"
: "text-muted-foreground hover:bg-accent/50"
)}>
<FolderIcon className="h-4 w-4" />
Projects
</span>
)}
</Link>
The useNavigate Hook
For programmatic navigation:
import { useNavigate } from "@tanstack/react-router";
function ProjectActions({ projectId }: { projectId: string }) {
const navigate = useNavigate();
const handleArchive = async () => {
await archiveProject(projectId);
// Navigate after action — fully type-safe
navigate({ to: "/projects", search: { status: "archived" } });
};
const handleEdit = () => {
navigate({
to: "/projects/$projectId",
params: { projectId },
search: { tab: "settings" },
});
};
return (
<div className="flex gap-2">
<Button onClick={handleEdit}>Edit</Button>
<Button variant="destructive" onClick={handleArchive}>Archive</Button>
</div>
);
}
Relative Navigation
// Navigate relative to current route
navigate({ to: ".", search: { page: 2 } }); // Same route, update search
navigate({ to: "..", }); // Parent route
navigate({ to: "/projects/$projectId", params: { projectId: "new" } }); // Absolute
Route Params
Accessing Params
// In a route component
function ProjectPage() {
const { projectId } = Route.useParams();
// projectId: string — fully typed
return <div>Project: {projectId}</div>;
}
// In any component (with the route path)
import { useParams } from "@tanstack/react-router";
function ProjectBreadcrumb() {
const { projectId } = useParams({
from: "/_authenticated/projects/$projectId",
});
return <span>{projectId}</span>;
}
Param Parsing
Transform params before they reach your component:
export const Route = createFileRoute("/_authenticated/projects/$projectId")({
parseParams: (params) => ({
projectId: params.projectId, // Could add validation/transformation here
}),
stringifyParams: (params) => ({
projectId: params.projectId,
}),
});
Redirects
import { redirect } from "@tanstack/react-router";
// In beforeLoad — runs before the route renders
export const Route = createFileRoute("/_authenticated")({
beforeLoad: async () => {
const session = await getSession();
if (!session) {
throw redirect({
to: "/login",
search: { redirect: location.pathname },
});
}
},
});
// Redirect based on data
export const Route = createFileRoute("/_authenticated/projects/$projectId")({
loader: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData(
projectQueryOptions(params.projectId)
);
// Redirect archived projects to the archive view
if (project.status === "archived") {
throw redirect({
to: "/projects",
search: { status: "archived" },
});
}
return project;
},
});
Navigation Guards
Prevent navigation when the user has unsaved changes:
import { useBlocker } from "@tanstack/react-router";
function ProjectForm() {
const [isDirty, setIsDirty] = useState(false);
useBlocker({
shouldBlockFn: () => isDirty,
withResolver: true,
});
return (
<form onChange={() => setIsDirty(true)}>
{/* form fields */}
</form>
);
}
Building a Navigation Component
// shared/components/layout/sidebar-nav.tsx
import { Link } from "@tanstack/react-router";
import {
LayoutDashboard,
FolderKanban,
CheckSquare,
Settings,
Bell,
} from "lucide-react";
const navItems = [
{ to: "/dashboard" as const, label: "Dashboard", icon: LayoutDashboard },
{ to: "/projects" as const, label: "Projects", icon: FolderKanban },
{ to: "/tasks" as const, label: "Tasks", icon: CheckSquare },
{ to: "/notifications" as const, label: "Notifications", icon: Bell },
{ to: "/settings" as const, label: "Settings", icon: Settings },
];
export function SidebarNav() {
return (
<nav className="space-y-1">
{navItems.map((item) => (
<Link
key={item.to}
to={item.to}
className="flex items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
activeProps={{
className: "bg-accent text-accent-foreground font-medium",
}}
>
<item.icon className="h-4 w-4" />
{item.label}
</Link>
))}
</nav>
);
}
Summary
- ✅
Linkcomponent provides compile-time route validation - ✅
useNavigatefor programmatic type-safe navigation - ✅ Active link styling through
activePropsor render functions - ✅ Route params are typed and accessible via
Route.useParams() - ✅ Redirects in
beforeLoadandloaderfor auth guards and data-driven routing - ✅ Navigation guards (
useBlocker) prevent losing unsaved changes