Dynamic Breadcrumbs in TanStack Router: The 'Non-DRY' Way
Stop repeating yourself. Learn how to colocate dynamic breadcrumbs inside your TanStack Router loaders for a cleaner, type-safe developer experience.
Building breadcrumbs usually feels like a chore—you’re either hardcoding strings in a giant config file or manually parsing URLs like it’s 2012.
In the 2025 “Ghost Town” of AI-generated code, I’m all about colocation. I want my breadcrumb logic to live exactly where my route lives. I’ve found a “non-DRY” way to compose breadcrumbs in TanStack Router that is clean, type-safe, and honestly, a bit of a joy to use.
The Secret: Put it in the Loader
Why manage a separate breadcrumb state when your route already knows who it is? By using the loader, we can define breadcrumbs alongside our data fetching.
export const Route = createFileRoute("/_authenticated/_layout/boards")({
component: BoardsPage,
loader: () => {
return {
breadcrumbs: linkOptions([
{
label: "Boards",
to: "/boards",
},
]),
};
},
head: (ctx) => ({
meta: [{ title: "Boards" }],
}),
});
The beauty here? If you need a dynamic breadcrumb (like a Board Name from an API), you just fetch it in the loader and pass it into the label. No more :id slugs in your UI.
The “Magic” Component
To render them, we use useMatches(). We filter for any route that has breadcrumbs in its loaderData and flatten them into a single list.
type BreadcrumbType = LinkProps & { label: string };
export function TsrBreadcrumbs() {
const matches = useMatches();
const breadcrumbs = matches
.filter((match) => isMatch(match, "loaderData.breadcrumbs"))
.flatMap((match) => match.loaderData?.breadcrumbs) as BreadcrumbType[];
return (
<nav className="flex gap-2 text-sm">
{breadcrumbs.map(({ label, ...linkProps }, index, arr) => {
const isTail = index === arr.length - 1;
return isTail ? (
<span key={index} className="font-bold">{label}</span>
) : (
<Link key={index} {...linkProps} className="text-blue-500 hover:underline">
{label} /
</Link>
);
})}
</nav>
);
}
Why this wins in 2025:
Zero Maintenance: Delete a route file? The breadcrumb disappears automatically.
Type Safety: linkOptions ensures your to paths actually exist. If you rename a route, TypeScript will yell at you before you even hit save.
One component in your header, one field in your loader, and voilà—dynamic navigation that doesn’t make you want to throw your laptop out the window.
Happy Coding!