Dynamic breadcrumbs in Next.js
tl;dr - Implementing dynamic breadcrumbs in Next.js using the app router.
Often, breadcrumbs are used to show the user’s current location within the site hierarchy. In a static site, breadcrumbs are typically hard-coded into the page. However, in a dynamic site, breadcrumbs need to be generated based on the current path. Additionally, sometimes one needs to set breadcrumbs asynchronously, such as when fetching data from an API. In this post, I’ll demonstrate one of many ways to implement dynamic breadcrumbs in Next.js when using the app router.
Getting started
We start by creating a new Next.js project with npx create-next-app
which we will use for demonstration.
For breadcrumbs to add value, we need to have multiple pages, ideally, with some navigational depth.
Without going into into too much detail, we will simply add a few pages representing categories of pets.
The possible navigation paths look like this:
Home -> Category -> Variant -> Example pet
Breadcrumbs
We can now proceed to add breadcrumbs to our project starting with the UI part of the breadcrumbs.
We will create two components, BreadcrumbsContainer
and BreadcrumbsItem
. The BreadcrumbsContainer
wraps
each BreadcrumbsItem
which represents a single part of the current path while the BreadcrumbsContainer
is
responsible for rendering the separator between each BreadcrumbsItem
.
const BreadcrumbsItem = ({
children,
href,
...props
}: BreadcrumbItemProps) => {
return (
<li {...props}>
<Link href={href} passHref>
{children}
</Link>
</li>
);
};
const BreadcrumbsContainer = ({
children,
separator = '/',
}: BreadcrumbsContainerProps) => (
<nav className="min-h-6 pb-6">
<ol className="flex items-center space-x-4">
{Children.map(children, (child, index) => (
<>
{child}
{index < Children.count(children) - 1
? <span>{separator}</span>
: null}
</>
))}
</ol>
</nav>
);
With these components we can already create a simple dynamic breadcrumb component which covers many use cases:
import { Children } from 'react';
import { usePathname } from 'next/navigation';
const BreadCrumbs = ({
children,
}: BreadcrumbsProps) => {
const paths = usePathname();
const pathNames = paths.split('/').filter((path) => path);
const pathItems = pathNames
.map((path, i) => ({
// Optionally you can capitalize the first letter here
name: path,
path: pathNames.slice(0, i + 1).join('/'),
}));
return (
<BreadcrumbsContainer>
{pathItems.map((item) => (
<BreadcrumbsItem key={item.path} href={`/${item.path}`}>
{item.name}
</BreadcrumbsItem>
))}
</BreadcrumbsContainer>
);
};
This component will render a list of breadcrumbs based on the current path. It is great for sites where each path segment has a meaningful name. However, in many cases, the path segments are just IDs or slugs without accessible meaning for the end user. It would be desirable to set the breadcrumbs independent from the current path. Here we will focus only on setting the last item of the breadcrumbs, though the concept can be extended to set all parts of the breadcrumbs.
Breadcrumbs with context
Extending on the previous example, we can add context to set the breadcrumbs component. This will enable us to set the last breadcrumbs item to a value of our choice.
const BreadCrumbsContext = createContext<Context>({
trailingPath: '',
setTrailingPath: () => {},
});
const BreadCrumbs = ({
children,
}: BreadcrumbsProps) => {
const paths = usePathname();
const [trailingPath, setTrailingPath] = useState('');
const context = useMemo(() => ({
trailingPath,
setTrailingPath,
}), [trailingPath]);
const pathNames = paths.split('/').filter((path) => path);
const pathItems = pathNames
.map((path, i) => ({
name: path,
path: pathNames.slice(0, i + 1).join('/'),
}));
if (trailingPath && pathItems.length > 0 && trailingPath !== pathItems[pathItems.length - 1].name) {
pathItems[pathItems.length - 1].name = trailingPath;
}
return (
<>
<BreadcrumbsContainer>
{pathItems.map((item) => (
<BreadcrumbsItem key={item.path} href={`/${item.path}`}>
{item.name === 'loading'
? <LoadingSpinner className="w-4 h-4" />
: item.name}
</BreadcrumbsItem>
))}
</BreadcrumbsContainer>
<BreadCrumbsContext.Provider value={context}>
{children}
</BreadCrumbsContext.Provider>
</>
);
};
The only changes made are the addition of the trailingPath
state and the BreadCrumbsContext.Provider
which will
wrap the page content. If a trailing path is set, it will replace the last item in the breadcrumbs giving us the
ability to set the last breadcrumb to a arbitrary value. To actually set the last breadcrumb we introduce the helper
useBreadCrumbs
which uses the breadcrumb context and sets the trailingPath
for us.
export const useBreadCrumbs = (trailingPath?: string) => {
const context = useContext(BreadCrumbsContext);
useEffect(() => {
context.setTrailingPath(trailingPath ? trailingPath : 'loading');
return () => context.setTrailingPath('');
}, [trailingPath, context]);
}
Using the Breadcrumbs component
To add breadcrumbs to our site we wrap the child elements in the layout.tsx
file
// ...
<BreadCrumbs>
{children}
</BreadCrumbs>
// ...
We can now set the last breadcrumb using the useBreadCrumbs
hook from any page.
const Page = () => {
useBreadCrumbs('Example pet');
return (
<div>
<h1>Example pet</h1>
</div>
);
};
This also works with asynchronous data fetching. For example, we can fetch the pet name from an API and set the breadcrumb when the data is available. It is straightforward to add a loading state while the asynchronous data is being fetched. A full example with some twerks can be found here.