Using Ramda to build ReactStrap Breadcrumbs

Using Ramda to build ReactStrap Breadcrumbs

I have begun using ReactStrap because it makes it simpler to build common bootstrap elements with React. At the same time, I have started to experiment with the Ramda functional Library to try to make our React/Redux code more declarative.

Recently, a colleague of mine pointed out a way to use Ramda in our Breadcrumb component and I wanted to share.

This is the component, pre-Ramda. It was using the Array#map method with an if statement inside to render the last breadcrumb item without the link. The last breadcumb item also needs the active property set.

import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
import { Breadcrumb, BreadcrumbItem } from "reactstrap";

const AppBreadcrumb = ({ breadcrumbs }) => (
  <Breadcrumb>
    <BreadcrumbItem><a href="/">Home</a></BreadcrumbItem>
    {
      breadcrumbs.map((bc, i, arr) => {
        if (i === arr.length - 1) {
          return (<BreadcrumbItem key={bc.path} active>{bc.text}</BreadcrumbItem>);
        }
        return (
          <BreadcrumbItem key={bc.path}>
            <Link to={bc.path}>{bc.text}</Link>
          </BreadcrumbItem>
        );
      })
    }

  </Breadcrumb>
);
AppBreadcrumb.propTypes = {
  breadcrumbs: PropTypes.arrayOf(PropTypes.shape({
    path: PropTypes.string.isRequired,
    text: PropTypes.string.isRequired
  })).isRequired
};
export default AppBreadcrumb;

While this code may be simple to the person that wrote it, it will require mental parsing by future readers of the code.

Some confusing things:

  • Why is the “if” statement that renders the active component first when it is really rendered last?
  • Why are we evaluating arr.length and using an index i? (The index really has nothing to do with this code.)
  • Why is so hard for me to see the active in <BreadcrumbItem key={bc.path} active>? This one of the key distinguishers but I have to mentally parse 3 different variables to grasp this.

The component was refactored into two separate component: ActiveBreadcrumbItem and LinkedBreadcrumbItem. These are very simple stateless functional components.

Next we can use two Ramda statements, one to render the linked items and one to render the active. The refactored code looks like this:

import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
import { Breadcrumb, BreadcrumbItem } from "reactstrap";

const R = require("ramda");

const ActiveBreadcrumbItem = ({ path, text }) => (
  <BreadcrumbItem key={path} active>{text}</BreadcrumbItem>
);

const LinkedBreadcrumbItem = ({ path, text }) => (
  <BreadcrumbItem key={path}>
    <Link to={path}>{text}</Link>
  </BreadcrumbItem>
);

const AppBreadcrumb = ({ breadcrumbs }) => (
  <Breadcrumb>
    <BreadcrumbItem><a href="/">Home</a></BreadcrumbItem>
    {R.map(LinkedBreadcrumbItem, R.slice(0, -1, breadcrumbs))}
    {R.pipe(R.last, ActiveBreadcrumbItem)(breadcrumbs)}
  </Breadcrumb>
);

ActiveBreadcrumbItem.propTypes = {
  path: PropTypes.string.isRequired,
  text: PropTypes.string.isRequired
};

LinkedBreadcrumbItem.propTypes = ActiveBreadcrumbItem.propTypes;

AppBreadcrumb.propTypes = {
  breadcrumbs: PropTypes.arrayOf(PropTypes.shape(ActiveBreadcrumbItem.propTypes)).isRequired
};

export default AppBreadcrumb;

While this code may take a minute to grok, it has the following advantage:

  • The order of the code is the same as the order of the rendered component.
  • It is more declarative since we are using R.slice to get the linked items and R.last to get the final element rather than evaluating an array index.
  • Exploits React’s functional components.