— React.js, JavaScript — 3 min read
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
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 yournpm
version is at least5.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-dom3# Since we are using TypeScript, we need to add React Router types4yarn add --dev @types/react-router-dom
Our project setup is done. Next, we will add a 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.
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.
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();
We need to add React Router to our app. Open src/index.tsx
, import Router
and wrap <App />
component within Router
component.
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.
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.
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.
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.
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;
To have nicer looking links, update src/index.css
with following styles:
1body {2 margin: 1.5em;3}4
5a {6 color: blue;7 text-decoration: none;8}9
10a:hover {11 color: violet;12}
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
.
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 BreadcrumbOutlet
s. 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.
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
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.
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 breadcrumbOutletId21 ) 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 breadcrumbOutlet36 );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.
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.
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
.
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.
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.
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.
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};