Dynamic breadcrumbs in Next.js using the app router

gcascio

gcascio

Posted on April 1, 2024

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
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

Using the Breadcrumbs component

To add breadcrumbs to our site we wrap the child elements in the layout.tsx file

// ...
<BreadCrumbs>
  {children}
</BreadCrumbs>
// ...
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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 and checkout this demo of the breadcrumbs.

💖 💪 🙅 🚩
gcascio
gcascio

Posted on April 1, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related