DEV Community

Josh Branchaud
Josh Branchaud

Posted on

A Little Bit of JavaScript: classnames

The classnames library is something I use nearly every day as a React developer. It is "a simple JavaScript utility for conditionally joining classNames together."

Here is a minimal example from their docs of how it is used:

var classNames = require('classnames');
classNames('foo', 'bar'); // => 'foo bar'
Enter fullscreen mode Exit fullscreen mode

For 99% of my use cases, it allows me to do the following things:

  • combine some static CSS class value with some computed value
  • combine an incoming classNames prop with other in-component classNames
  • conditionally include CSS class values through an object literal

The library likely does a few other more specific things and gracefully handles a variety of edge cases, but I bet we can get the majority of the behavior we need with just a little bit of JavaScript.

But first, let's look at a more real-world example of JSX that we want to support:

import React from "react";
import cx from "classnames";

export default function Button(props) {
  const { size, className, disabled, ...rest } = props;
  const sizeClassName = `btn-${size}`;

  return (
    <button
      className={cx("btn", sizeClassName, className, {
        ["btn-disabled"]: disabled
      })}
    >
      {/* ... */}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

Our focus will be on the cx (my preferred shorthand when importing classnames) value we are computing. Here is an example of what we might expect:

const size = "medium";
const className = "text-bold";
const disabled = true;

cx("btn", sizeClassName, className, {
  ["btn-disabled"]: disabled }
);
//=> "btn btn-medium text-bold btn-disabled"
Enter fullscreen mode Exit fullscreen mode

Here is a little JavaScript to make this utility ourselves:

function cx(...classnames) {
  return classnames
    .map(item => {
      if (typeof item === "string") {
        return item;
      }
      if (typeof item === "object") {
        return Object.keys(item)
          .map(key => {
            return item[key] ? key : void 0;
          })
          .join(" ");
      }
      return void 0;
    })
    .join(" ");
}
Enter fullscreen mode Exit fullscreen mode

I'll explain a bit more below, but feel free to check out the interactive example as well.

After prettier does its thing, this comes out to 17 lines of code. Nevertheless, there is a lot going on here, so let's look at it piece by piece.

function cx(...classnames) {
Enter fullscreen mode Exit fullscreen mode

Use of the spread operator gathers one or more arguments into an array referenced by the classnames variable.

return classnames
  .map(item => { /* logic to process args here ... */ })
  .join(" ");
Enter fullscreen mode Exit fullscreen mode

We then map over each argument in classnames. Some logic that we will look at in a second will determine each string part that will make up the resulting className value. These are joined together with spaces between and returned.

Now for the guts of the map function:

(item) => {
  if (typeof item === "string") {
    return item;
  }
  if (typeof item === "object") {
    /* handle object literals here ... */
  }
  return void 0;
}
Enter fullscreen mode Exit fullscreen mode

The simple case is if an argument is a string; we'll just return it as is. If we encounter an object literal (e.g. { cats: true }), then we'll have to do some special processing of that. Anything else we are choosing to ignore, so we'll return void 0 (which is undefined).

Here is how we process an object literal argument:

if (typeof item === "object") {
  return Object.keys(item)
    .map(key => {
      return item[key] ? key : void 0;
    })
    .join(" ");
}
Enter fullscreen mode Exit fullscreen mode

Mapping over each key-value pair in the object, we include the key if it is paired with a truthy value, otherwise we return undefined. The result of this mapping is joined together with a single space as the delimiter. This mapped and joined string will then get joined into the string that ultimately is returned.

Now that we've looked at all the parts, let's look at the whole thing together again:

function cx(...classnames) {
  return classnames
    .map(item => {
      if (typeof item === "string") {
        return item;
      }
      if (typeof item === "object") {
        return Object.keys(item)
          .map(key => {
            return item[key] ? key : void 0;
          })
          .join(" ");
      }
      return void 0;
    })
    .join(" ");
}
Enter fullscreen mode Exit fullscreen mode

You may not be looking to replace your classnames dependency with a hand-rolled version anytime soon, but it's nice to remember how far you can get with just a little bit of JavaScript.

Happy Holidays 🎄

Top comments (2)

Collapse
 
sidvishnoi profile image
Sid Vishnoi

A good, to-the-point article. 👐
Question: why use void 0 instead of undefined, which has clear meaning?

Collapse
 
jbranchaud profile image
Josh Branchaud

I've picked up the habit from my current team to use void 0. My understanding is that it is supposed to be safer. They are essentially interchangeable in modern browsers. I agree it'd be clearer to use undefined.