Skip to content
Efe Behar

💻 Breadcrumb component with React Portals

React.js, JavaScript3 min read

What is breadcrumb?

A breadcrumb is a navigation component consists of a list of links to the parent pages of the current page in hierarchical order. It helps users know their current location on the page, and provide quick navigation to parent pages. Breadcrumbs are very useful when we have nested routing. We will create a breadcrumb component taking advantage of React's dynamic routing and Portal API.

React Portal Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. Visit official docs to learn more about Portals

Setting up a project

Lets create a new React project using create-react-app. I will use TypeScript to make code easier to read and understand. To create new React project with TypeScript support, use the code below:

1npx create-react-app [your-app-name] --template typescript

To use npx , make sure your npm version is at least 5.2.0. More about npx

Once the whole setup process is done, let's add React Router to our project.

1cd [your-app-name]
2yarn add react-router-dom
3# Since we are using TypeScript, we need to add React Router types
4yarn add --dev @types/react-router-dom

Our project setup is done. Next, we will add a data source.

Adding data source

We will create a Zoo application, that allows users to browse animals in the Zoo and see the animal's name and scientific name. It is always nice to work with some data instead of foo/bar.

Link to animals data

I generated this data using Mockaroo.

I will put our data into src/services/data.ts. Let's create a service to share our data with our components. We will have two methods, one for getting all existing animals, the second one for getting an animal by unique animal ID.

src/services/Animal.service.ts
1import data from "./data";
2
3export type Animal = typeof data[0];
4
5class AnimalService {
6 public getAnimals(): Animal[] {
7 return data;
8 }
9
10 public getAnimalById(animalId: string | number): Animal {
11 return data.filter(({ id }) => id === Number(animalId))[0];
12 }
13}
14
15export default new AnimalService();

Adding routes

We need to add React Router to our app. Open src/index.tsx, import Router and wrap <App /> component within Router component.

src/index.tsx
1...
2import { BrowserRouter as Router } from "react-router-dom";
3...
4ReactDOM.render(
5 <React.StrictMode>
6 <Router>
7 <App />
8 </Router>
9 </React.StrictMode>,
10 document.getElementById("root")
11);
12...

Let's add our routes. Our app consists of three pages, Home, Animals, and Animal details. Let's create new pages directory in the src/ and add Home, Animals, and Animal page.

Home page contains greeting text and link to the /animals page.

src/pages/Home.tsx
1import React from "react";
2import { Link } from "react-router-dom";
3
4export default () => (
5 <section>
6 <h2>Welcome to our Zoo!</h2>
7 <Link to="/animals">See our animals</Link>
8 </section>
9);

Animals page represents a list of animals in the Zoo. User can select one of the animals and navigate to detailed animal page.

src/pages/Animals.tsx
1import React from "react";
2import { Switch, Route, useRouteMatch, Link } from "react-router-dom";
3import Animal from "./Animal";
4import AnimalService from "services/Animal.service";
5
6export default () => {
7 const match = useRouteMatch();
8
9 return (
10 <section>
11 <Switch>
12 <Route exact path={match.url}>
13 <h2>Our animals</h2>
14 <ul>
15 {AnimalService.getAnimals().map(({ id, name }) => (
16 <li key={id}>
17 <Link to={`/animals/${id}`}>{name}</Link>
18 </li>
19 ))}
20 </ul>
21 </Route>
22 <Route exact path={`${match.url}/:animalId`} component={Animal} />
23 </Switch>
24 </section>
25 );
26};

Animal page shows detailed information about selected animal.

src/pages/Animal.tsx
1import React from "react";
2import { useRouteMatch, Link } from "react-router-dom";
3import AnimalService from "services/Animal.service";
4
5export default () => {
6 const { params } = useRouteMatch<{ animalId: string }>();
7
8 const animal = AnimalService.getAnimalById(params.animalId);
9
10 if (!animal)
11 return (
12 <p>
13 Bad animal. <Link to="/animals">Return to animals</Link>
14 </p>
15 );
16
17 const { name, scientific_name } = animal;
18
19 return (
20 <section>
21 <div>
22 <b>Name: </b>
23 {name}
24 </div>
25 <div>
26 <b>Scientific Name: </b>
27 {scientific_name}
28 </div>
29 </section>
30 );
31};

We created our pages. Now, we should modify App component.

src/App.tsx
1import "./App.css";
2import React from "react";
3import { Switch, Route } from "react-router-dom";
4import Animals from "pages/Animals";
5import Home from "pages/Home";
6
7function App() {
8 return (
9 <div>
10 <header>
11 <h1>Breadcrumbs with React Portals</h1>
12 {/* Breadcrumb will be here */}
13 <hr />
14 </header>
15
16 <Switch>
17 <Route path="/animals" component={Animals} />
18 <Route path="/" component={Home} />
19 </Switch>
20 </div>
21 );
22}
23
24export default App;

A little bit styling

To have nicer looking links, update src/index.css with following styles:

src/index.css
1body {
2 margin: 1.5em;
3}
4
5a {
6 color: blue;
7 text-decoration: none;
8}
9
10a:hover {
11 color: violet;
12}
Yaay! We are done with our routing!

Breadcrumbs

Our breadcrumb will have two components. The first component will be BreadcrumbOutlet, this is a component where we will place our breadcrumbs in the UI. The second one is the Breadcrumb component. Using this component we will populate our BreadcrumbOutlet.

BreadcrumbOutlet

This is a component where we will place our breadcrumbs in the UI. This component accepts only one optional property called id, which will give us the ability to have multiple BreadcrumbOutlets. Since id property is optional, we need to assign a default id which is default-breadcrumb. We are attaching this id to the ol list component. We will need to find this component by its id to mount our breadcrumbs into it.

src/components/breadcrumb/BreadcrumbOutlet.tsx
1import React from "react";
2
3export interface BreadcrumbOutletProps {
4 id?: string;
5}
6
7const BreadcrumbOutlet: React.FunctionComponent<BreadcrumbOutletProps> = ({
8 id = "default-breadcrumb",
9}) => {
10 return (
11 <nav aria-label="breadcrumb" className="breadcrumb">
12 <ol id={id} />
13 </nav>
14 );
15};
16
17export default BreadcrumbOutlet;

Breadcrumb

Breadcrumb component is a component that renders nothing directly but populates BreadcrumbOutlet with breadcrumb items using Portals. We can use this component anywhere in the application because of Portals able to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

src/components/breadcrumb/Breadcrumb.tsx
1import React, { useState, useEffect } from "react";
2import { createPortal } from "react-dom";
3import { useRouteMatch, Link } from "react-router-dom";
4
5interface BreadcrumbProps {
6 breadcrumbOutletId?: string;
7 link?: string;
8}
9
10const Breadcrumb: React.FunctionComponent<BreadcrumbProps> = ({
11 breadcrumbOutletId = "default-breadcrumb",
12 link,
13 children,
14}) => {
15 const [breadcrumbOutlet, setBreadcrumbOutlet] = useState<HTMLOListElement>();
16 const match = useRouteMatch();
17
18 useEffect(() => {
19 const outletElement = document.getElementById(
20 breadcrumbOutletId
21 ) as HTMLOListElement;
22 if (outletElement) {
23 setBreadcrumbOutlet(outletElement);
24 }
25 }, [breadcrumbOutletId]);
26
27 if (!breadcrumbOutlet) {
28 return null;
29 }
30
31 return createPortal(
32 <li>
33 <Link to={link || match.url}>{children}</Link>
34 </li>,
35 breadcrumbOutlet
36 );
37};
38
39export default Breadcrumb;

Our breadcrumb has two optional properties. One of them is breadcrumbOutletId to find a DOM node (this is ol element in our case), it is set to default-breadcrumb as default. Another property is a link property to assign a link to the breadcrumb, as a default it is set to current URL of the page where breadcrumb item is initiated.

At the top of the component, we are initiating a new state using useState hook to keep the target list element, where we will mount our breadcrumb, also match variable to retrieve some router info using useRouteMatch hook. In the next step, we are looking for our outlet element using breadcrumbOutletId and updating the state. As the last step, we are returning a new portal using the createPortal method from the react-dom module. The first argument of createPortal is any renderable React child, such as an element, string, or fragment. The second argument is a DOM element. We pass the list item element with a link in it as a first argument, and out breadcrumbOutlet as a second. In the Link element, we have to property set to match.url as a default. match.url gives us the current URL of the page.

Breadcrumb styles

Our breadcrumb logic is done, but lack of styles. Create a css file in the src/components/breadcrumb directory called Breadcrumb.css and add following styles.

src/components/breadcrumb/Breadcrumb.css
1.breadcrumb ol {
2 display: flex;
3 list-style: none;
4 padding: 0;
5}
6
7.breadcrumb ol li:not(:last-child)::after {
8 content: ">";
9 margin: 0 10px;
10 color: black;
11}
12
13.breadcrumb ol li:not(:only-child):last-child a {
14 color: black;
15 pointer-events: none;
16 font-weight: bold;
17}

Import Breadcrumb.css into BreadcrumbOutlet.tsx.

src/components/breadcrumb/BreadcrumbOutlet.tsx
1import React from "react";
2import './Breadcrumb.css';
3...
4...

Now, our breadcrumb component is ready. Time to use it! First, we will add BreadcrumbOutlet to render our breadcrumbs on the page and breadcrumb item for the root route.

src/App.tsx
1...
2import BreadcrumbOutlet from "components/breadcrumb/BreadcrumbOutlet";
3import Breadcrumb from "components/breadcrumb/Breadcrumb";
4
5function App() {
6 return (
7 <div>
8 <header>
9 <h1>Breadcrumbs with React Portals</h1>
10 <BreadcrumbOutlet />
11 <Breadcrumb>Home</Breadcrumb>
12 <hr />
13 </header>
14
15 <Switch>
16 <Route path="/animals" component={Animals} />
17 <Route path="/" component={Home} />
18 </Switch>
19 </div>
20 );
21}
22...

Next, we will add breadcrumb to the Animals page.

src/pages/Animals.tsx
1...
2import Breadcrumb from "components/breadcrumb/Breadcrumb";
3
4export default () => {
5 ...
6 return (
7 <section>
8 <Breadcrumb>Animals</Breadcrumb>
9 ...
10 </section>
11 );
12};

As a last thing, we will add breadcrumb for Animal details page and we will show animal's name as a breadcrumb title.

src/pages/Animal.tsx
1...
2import Breadcrumb from "components/breadcrumb/Breadcrumb";
3
4export default () => {
5 ...
6 const { name, scientific_name } = animal;
7
8 return (
9 <section>
10 <Breadcrumb>{name}</Breadcrumb>
11 ...
12 </section>
13 );
14};
Our breadcrumb implementation is done! Lets test it.

Link to the source code