Skip to main content

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.

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>
<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;
},
});

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

  • Link component provides compile-time route validation
  • useNavigate for programmatic type-safe navigation
  • Active link styling through activeProps or render functions
  • Route params are typed and accessible via Route.useParams()
  • Redirects in beforeLoad and loader for auth guards and data-driven routing
  • Navigation guards (useBlocker) prevent losing unsaved changes