Dynamic breadcrumbs in Next.js using the app router
Tags: nextjs, breadcrumbs, webdev, typescript
Published: 2024-04-01 20:20:15

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.

<br />
<br />

## Getting started

<br />

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
```

<br />
<br />

## Breadcrumbs

<br />

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`.

```tsx
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:

```tsx
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.

<br />
<br />

## Breadcrumbs with context

<br />

Extending on the previous example, we can add [context](https://react.dev/learn/passing-data-deeply-with-context) to set the breadcrumbs component. This will enable us to set the last breadcrumbs item to a value of our choice.

```tsx
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.

```tsx
export const useBreadCrumbs = (trailingPath?: string) => {
  const context = useContext(BreadCrumbsContext);

  useEffect(() => {
    context.setTrailingPath(trailingPath ? trailingPath : 'loading');
    return () => context.setTrailingPath('');
  }, [trailingPath, context]);
}
```

<br />
<br />

## Using the Breadcrumbs component

<br />

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

```tsx
// ...
<BreadCrumbs>
  {children}
</BreadCrumbs>
// ...
```

We can now set the last breadcrumb using the `useBreadCrumbs` hook from any page.

```tsx
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](https://github.com/gcascio/next-breadcrumbs) and checkout this [demo](https://next-breadcrumbs.vercel.app/) of the breadcrumbs.