You don't know the classNames library

areknawo

Arek Nawo

Posted on March 10, 2021

You don't know the classNames library

Let me contradict the very title of this post from the start by saying that you probably know the classNames library very well.

This tiny open-source library, originally created by JedWatson, is nothing but a versatile string “concatenator.” Currently sitting at over 6M weekly downloads, it rose to popularity alongside React - the UI library that it’s most commonly used with.

classNames and React popularity growth

classNames and React popularity growth

As the name implies, it’s primarily meant for dealing with CSS classNames (very common in React and any other JSX-based UI framework), although you can use it for any kind of string concatenation.

The novelty of tiny NPM packages

But you most likely know it all. After all, given classNames and React popularity, there’s a high chance that you’ve used it before. But yeah, about this popularity.

It’s not uncommon to see tiny packages have insanely high download stats on NPM. You most likely have heard the stories of NPM packages with even less than 20 lines of code, breaking the internet because of the slightest change. Take is-promise for example - sitting at around 10M weekly downloads (mainly from its highly-popular dependents) - with its largest CJS version measuring 5 lines of code (LOCs):

module.exports = isPromise;
module.exports.default = isPromise;

function isPromise(obj) {
  return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}
Enter fullscreen mode Exit fullscreen mode

Now, such dependence might seem scary, and rightfully so. The jokes about black-hole node_modules folders are true for a reason. Even though you might not depend on such packages directly, packages that you depend on might do so, one, or more likely, multiple levels deep.

classNames isn’t exactly that tiny, with around 50 LOCs at its “core” version and between 200 - 300 in total (all versions + TypeScript typings). This is what I consider a “sane” threshold for package dependence. Besides, the library is very stable and has proven itself over the years.

Classnames syntax

The usefulness of the classNames library comes from its syntax. All it is is a single function, taking different types of values as arguments and spitting out a single string based on them.

The README does a great job of showcasing this versatility:

import classNames from "classnames";

const arr = ["b", { c: true, d: false }];
const buttonType = "primary";

classNames("foo", "bar"); // => "foo bar"
classNames("foo", { bar: true }); // => "foo bar"
classNames({ "foo-bar": true }); // => "foo-bar"
classNames({ "foo-bar": false }); // => ""
classNames({ foo: true }, { bar: true }); // => "foo bar"
classNames({ foo: true, bar: true }); // => "foo bar"

// lots of arguments of various types
classNames("foo", { bar: true, duck: false }, "baz", { quux: true }); // => "foo bar baz quux"

// other falsy values are just ignored
classNames(null, false, "bar", undefined, 0, 1, { baz: null }, ""); // => "bar 1"

classNames("a", arr); // => "a b c"

classNames({ [`btn-${buttonType}`]: true }); // => "btn-primary"
Enter fullscreen mode Exit fullscreen mode

This library's simplicity and versatility is probably something that you don’t really think about - you just use it, making it run thousands or even millions of times throughout your projects.

Performance

It might cause some concerns about performance. The author is aware of that, which is clearly stated in the README:

We take the stability and performance of this package seriously, because it is run millions of times a day in browsers all around the world. Updates are thoroughly reviewed for performance impacts before being released, and we have a comprehensive test suite.

However, it’s clear that a function call will never be faster than nothing but a simple string, and even though it seems like a micro-optimization, it’s also a code readability concern.

// pointless
classNames("foo", "bar"); // => "foo bar"
Enter fullscreen mode Exit fullscreen mode

So, it’s important not to get into a loop-hole and know when and how to use the library responsibly. In simpler cases, see if a plain string, ES6 template literal, or a conditional operator won’t do the job. Don’t waste performance, but also don’t over-optimize.

// make it better
classNames("foo", "bar"); // => "foo bar"
classNames(condition ? "foo" : "bar"); // => condition ? "foo" : "bar"
classNames(foo, bar); // => `${foo} ${bar}`
Enter fullscreen mode Exit fullscreen mode

Versatility

Apart from using classNames only when necessary, there’s still a lot to gain from using it properly. The biggest factor here is the versatility, which can often cause you to go with the sub-optimal way for a given scenario.

As noticed above, you can supply as many arguments as you want from which falsy values are ignored, strings are joined, arrays recursively flattened and processed, and objects’ keys joined if their values are truthy.

You can use these properties not necessarily to improve the performance but rather the readability and “writing comfort” of your code (aka “development experience”). As for some advice:

// use separate strings for base classes
classNames("foo", { bar: condition } /*...*/);
// provide multiple arguments instead of an array
classNames(
  "foo",
  {
    /*...*/
  },
  condition ? "a" : "b"
);
/* use conditional operator for switching between classes
 and object or "AND" operator for turning a single one on and off */
classNames(
  condition ? "a" : "b",
  { c: secondCondition },
  thirdCondition && "d"
);
Enter fullscreen mode Exit fullscreen mode

These are just a few tips from the top of my mind, which I personally use. It’s common to use an unnecessary array or to put base classes into an object property name with ugly true on its right, or to switch between sets of classes through an object with property values going like condition, !condition. None of those issues are particularly disturbing, but it’s worth remembering that there’s some room for improvement.

Classnames alternate versions

You might not have known, but classNames comes with 2 alternate versions of itself. Both serve pretty the same general purpose but also provide additional features.

dedupe

As the name implies, the dedupe version deals with duplicates in the generated string. It removes duplicate substrings and only respects the last provided setting for the particular substring.

import classNames from "classnames/dedupe";

classNames("foo", "foo", "bar"); // => 'foo bar'
classNames("foo", { foo: false, bar: true }); // => 'bar'
Enter fullscreen mode Exit fullscreen mode

Due to the complexity of deduping, this version is said to be 5x slower. Because of that, I don’t really recommend you use it for your classNames unless you’ve got a really specific reason. It still can be useful for generic string concatenation, though.

bind

The second version is targeted towards users of CSS Modules. When importing your object of CSS classes, this version allows you to “bind” them so that you can reference them by their custom name instead of the real one.

import classNames from "classnames/bind";

const styles = {
  foo: "abc",
  bar: "def",
  baz: "xyz",
};
const cx = classNames.bind(styles);

cx("foo", ["bar"], { baz: true });
Enter fullscreen mode Exit fullscreen mode

This method can save you some typing - no need to always access the properties from the imported object. However, it does introduce some additional performance loss (although really tiny), can confuse new-comers, requires creating a new “instance” of classNames function, and will cause you to lose potential autocompletion and other kinds of editor support (included TypeScript typings are very generic).

With that said, you should only use this version when you have a lot, and I mean a lot, of CSS module-imported classes to deal with (which you shouldn’t, by the way, it’s not “ergonomic”)

Just use clsx

Now, as a free tip to at least 2x the performance of the thousands of classNames calls you probably make, just switch to clsx. It’s a similar library to classNames, but a bit more fresh-y, and with even fewer LOCs.

The API is identical to the classNames one, but without the additional versions (which you probably don’t need anyway). And while the performance gains might not be noticeable, it still means that there’s speed left on the table, and the multitude of calls can quickly add up to something more.

Thoughts?

It feels a bit crazy to write a whole article about a one-function utility library. However, given how popular classNames is, how often it’s used and how almost unnoticeable it is, I think it deserved a bit in-depth look. Such small libraries and open-source tools are what’s powering today’s Web, and thus it’s important to keep try of your dependencies, know them well, and know how to optimize them.

Anyway, that’s been it! I hope you enjoyed this crazy ride and maybe - just maybe - learned something new today. If so, let me know down in the comments. Be sure to follow me on Twitter, Facebook, or through my newsletter for more bonkers web devs stories like this one and some more sane ones! Oh, and maybe start writing your own with CodeWrite!

Thanks for reading, and happy class-naming.

💖 💪 🙅 🚩
areknawo
Arek Nawo

Posted on March 10, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related