Skip to content

Instantly share code, notes, and snippets.

@heygrady
Last active September 5, 2024 01:56
Show Gist options
  • Save heygrady/316bd69633ce816aee1ca24ab63535db to your computer and use it in GitHub Desktop.
Save heygrady/316bd69633ce816aee1ca24ab63535db to your computer and use it in GitHub Desktop.

Using the classnames.bind method

Many people who work with React are familiar with the excellent classnames library. If you aren't familiar, it provides a simple function for gluing classnames together. In web programming in general, there are many times that we need to add or remove multiple classes based on conditional logic. The classnames library makes this easy.

More and more developers are embracing CSS Next and the power of CSS modules. However, when you add CSS modules to your react components, working with classnames gets more difficult. Typically, CSS modules is implemented with class name mangling. Transforming human readable class name strings into unique identifiers helps ensure that every class name in your app is unique.

This means that you can write your component CSS in isolation without worrying about the dreaded class name collisions that have plagued CSS developers since the dawn of the internet. CSS modules are like BEM without all of the hassle. The downside is that applying the class to your components is harder — the class name has been mangled behind the scenes but you still need to use it. We'll see below how CSS modules supplies a styles object that provides a simple mapping of the human readable class names to the unique versions created by webpack.

Thankfully, the classnames library ships with an alternative method for binding the classnames func to the styles object. This makes it very easy to work with the human readable names without having to wrestle with a mapping object. You can read more about this function in the classnames docs.

The purpose of this document is to showcase some examples of why that function exists, how to use it and how it works.

https://github.com/JedWatson/classnames#alternate-bind-version-for-css-modules

Example styles

For each of the examples below, presume we're importing the following CSS file as styles. The styles themselves are unimportant for this example. We're focusing here on the class names that it exports. Discussing the finer details of how CSS modules works is outside the scope of this document. However, at a high-level, presume that webpack will ensure that the each class name in this file is re-written to be totally unique. This avoids any chances of accidental class name collisions and is one of the main benefits of CSS modules.

.something { color: deeppink; }
.selected { font-weight: bold; }
.something-else { color: goldenrod; }

/* remember this! we're laying a trap */
.something-global { color: black; }

/* also make note that we're not adding ".something-local" */

Note: CSS modules can be configured a few different ways. To be backwards compatible, the spec suggests making styles global by default. However, for our purposes, presume we have configured classes to be local by default. Meaning, presume that a class is automatically wrapped in :local() and that you need to specify :global() to get an unmangled classname. You can read more about scoping in the webpack css-loader docs.

Example 0: before CSS modules

Remember the past? Before we used CSS modules it was really easy to add classes to stuff. Below we are not importing any styles. We're using classnames only to deal with the awkward syntax of adding the selected class. For this example, presume that the styles have been included in some global CSS file and added to the page for us.

Below you can see that it's really easy to set boolean classnames when you strategically name your boolean props. This makes your code very concise and easy to read. You can read more about using classnames in the docs.

import classnames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';

const Something = ({ selected }) => {
  return (
    <div className={classnames('something', { selected })}>
      Hello
      <span className="global-style something-else">
        World
      </span>
    </div>
  );
};

Something.propTypes = {
  selected: PropTypes.bool
};

export default Something;

Note: It's important to recognize that the selected prop isn't required and doesn't have a default value. When designing React components it's a good practice to let undefined values be undefined. If a boolean prop isn't specified then it will be undefined, which is a falsey value. In that cases, the classnames func will not apply the selected class name. So, the absence of the prop means the absence of the class name — pretty cool!

Adding a selected class without classnames

Although classnames has a somewhat awkward API for setting boolean classnames, it's way better than doing it by hand. A little magic goes a long way.

Below we're demonstrating a sampling of ways that we could construct a boolean classname without using the magic of the classnames function.

// we could put a ternary in an interpolated string
const classname = `something${ selected ? ' selected' : '' }`;

// we could capture the ternary in a variable (to hopefully enhance readability)
const selectedClassName = selected ? ' selected' : '';
const classname = `something${selectedClassName}`;

// we could use a bitwise operator
const classname = `something${selected && ' selected'}`;

// we could join an array and hope for the best
const classname = ['something', selected && 'selected'].join(' '); // <-- will have an empty trailing space if selected is falsey

Note: We're going to great lengths above to avoid unnecessary spaces between and after class names. This isn't strictly necessary. Unnecessary spaces won't affect rendering in any meaningful way. Many developers, however, prefer to keep things tidy. If you don't care about spaces, array joining is probably the best bet. It's worthwhile to notice how similar the array joining method looks to the classnames API.

let selected = true;

['something', selected && 'selected'].join(' '); // --> 'something selected'
classnames('something', { selected }); // --> 'something selected'

selected = false;

['something', selected && 'selected'].join(' '); // --> 'something ' (notice the space)
classnames('something', { selected }); // --> 'something' (no space! how tidy!)

Example 1: CSS Modules, without classnames

Let's look at the same example component from before, except we'll use CSS modules to import our styles. For fun, let's see what it looks like without using the classnames library. Because we aren't using a helper function, we're forced to use string interpolation (or other tricks) to glue classnames together.

This is probably the most explicit way of doing things. Any developer would be able to read this code and figure out what's going on. There isn't an API to learn; it's all just plain JS. No magic here. The downside is that it's somewhat verbose and slightly ugly because of the syntax of string interpolation.

Bad things:

  • This is pretty hard to read compared to the example above.
  • Less ugly alternatives, like array joining, add the overhead of creating arrays to create a string.

Good things:

  • No specialized API to learn.
  • Very straight-forward. Just plain JS.
// <-- not importing classnames
import PropTypes from 'prop-types';
import React from 'react';

import styles from './styles.css'; // <-- CSS modules

const Something = ({ selected }) => {
  const selectedClassName = selected ? ` ${styles.selected}` : ''; // <-- captured in a variable because it's too long
  return (
    <div className={`${styles.something}${selectedClassName}`)}>
      Hello
      <span className={`global-style ${styles['something-else']}`}>
        World
      </span>
    </div>
  );
};

Something.propTypes = {
  selected: PropTypes.bool
};

export default Something;

Note: It's important to notice the syntax for styles['something-else'] above. That syntax is the only way to reference object keys with disallowed characters. Because of that awkward syntax, many developers feel a pressure to use camelCased class names in their CSS when using CSS modules. The only problem is that CSS has a long history of preferring dash-cased class names. For seasoned developers, this can be more than a little awkward. It simply looks wrong to see camelCased class names in a CSS file (maybe we should get over it). We'll see below how to alleviate that pressure and avoid going against some deeply ingrained css class name naming dogma.

What CSS Modules is returning

Let's take a short moment to dissect what CSS modules is returning in the styles object. Many developers start using the object without really taking a moment to look under the hood. The styles object is just a key-value pair of the original style name and the mangled style created by webpack. It's good to keep this in mind as we review the examples further below.

Below you'll need to use your imagination an little bit. Webpack can be configured to apply any number of patterns for ensuring unique class names are generated. The exact pattern is up to the developer who configures webpack for your project. Because that configuration is different from project to project, we're simplifying the conversation and using __mangled__ as a prefix to represent the unique class name produced by the webpack css-loader. As a stylistic flourish, we refer to this unique classname as the "mangled" version, to indicate that it has be altered so as to make it unrecognizable.

So, when you see __mangled__something, imagine that you're looking at a unique string produced by webpack. The webpack css-loader docs show a very unique string like _23_aKvs-b8bW2Vg3fwHozO to make this same point. For simplicity and readability, we're preferring to use __mangled__ to stand in for that unreadable identifier.

import { isEqual } from 'lodash';
import styles from './styles.css'; // <-- webpack returns an object that maps the original name to the mangled name

// let's create our own object
const fakeStyles = {
  something: '__mangled__something', // <-- use your imagination here (presume we're able to guess what webpack would generate)
  selected: '__mangled__selected',
  'something-else': '__mangled__something-else',
  'something-global': '__mangled__something-global', // <-- note: not really global
};

// the styles object is just a key-value map
isEqual(styles, fakeStyles); // --> true

// try it yourself!
console.log(styles); // <-- you can do this right now in the component you're working on!

Note: In the example above, we can see that our something-global class was actually transformed into a very local class. This is the first part of the trap that we're laying. Also, notice that something-local was not transformed because it wasn't actually placed in our CSS file. Keep these two things in mind, we'll come back to it further below.

Example 2: with classnames (no binding)

When we add classnames back to the mix, things get a little bit better. Using classnames cleans up some of the messy string interpolation and completely removes the need to capture the selected classname in a variable (or does it?). However, using the styles object presents some of its own troubles.

On the surface these issues have nothing to do with classnames; styles is coming from CSS Modules. All classnames is doing is gluing strings together for you. The main issue is that we're unable to use the class name string directly. Instead, we need to reference the mangled class name using the styles mapping object. We'll see in later examples how to solve some of these issues.

You can see in the example below that converting to use CSS modules introduces some awkward syntax.

Bad things:

  • Pressure to use camelCased classnames in css. Consider classnames(styles['something-else']) versus classnames(styles.somethingElse)
  • Verbose when setting boolean classnames. Consider classnames({ [styles.selected]: selected }
  • Much more verbose than using classnames without CSS modules. Remember classnames('something-else') from above? Compare that to classnames(styles.something).
  • Fails in cases where styles is undefined (some edge cases in tests and SSR).

Good things:

  • Very explicit about where a specific classname is coming from. No potential for confusion between imported CSS module styles and global styles (more on that later).
  • Decidedly easier to read than not using classnames at all.
import classnames from 'classnames'; // <-- better than nothing
import PropTypes from 'prop-types';
import React from 'react';

import styles from './styles.css';

const Something = ({ selected }) => {
  return (
    <div className={classnames(styles.something, { [styles.selected]: selected })}>
      Hello
      <span className={classnames('global-style', styles['something-else'])}>
        World
      </span>
    </div>
  );
};

Something.propTypes = {
  selected: PropTypes.bool
};

export default Something;

Note: Using the styles object directly with classnames can lead to some very awkward syntax. Consider the next example below.

import classnames from 'classnames';
import styles from './styles.css';

let somethingElse = true;

// pretend we want to conditionally apply 'something-else'
classnames({ [styles['something-else']: somethingElse }); // -> something-else

Example 3: with classnames.bind

In the above example, we're using the styles object every time we need to reference a local classname. This can be quite cumbersome. If you're converting an old component to use CSS modules you can actually increase the files size (and reduce the readability) quite a bit.

Thankfully, classnames ships with an alternative bind method that's designed specifically to make working with CSS modules much less cumbersome. It requires a small bit of extra set up (and has one important caveat) but it gets us closer to the less verbose syntax we enjoyed in the days before CSS Modules. Of course, there are numerous benefits of using CSS Modules. So, why not make it easy to work with?

Bad things:

  • If you see cx('something') you will need to refer to the styles.css to be sure you're not accidentally referencing a global class (more on this below).

Good things:

  • More like using classnames without CSS Modules
  • Using cx('something') is significantly fewer characters than classnames(styles.something)
  • Using cx({ selected }) is significantly less cumbersome/error prone than classnames({ [styles.selected]: selected})
  • In situations where styles is undefined (some edge cases in tests and SSR), the bound cx function falls back silently to global classes.
  • Using the bound cx version makes it less painful to use dash-cased strings, consider cx('something-else') versus classnames(styles['something-else']). It essentially removes the pressure to use camelCased strings for classnames.

Note: Falling back to global styles is maybe a good thing or a bad thing... depending on why the styles object was undefined.

import classnames from 'classnames/bind'; // <-- notice bind
import PropTypes from 'prop-types';
import React from 'react';

import styles from './styles.css';

const cx = classname.bind(styles); // <-- explicitly bind your styles

const Something = ({ selected }) => {
  return (
    <div className={cx('something', { selected })}>
      Hello
      <span className={cx('global-style', 'something-else')}>
        World
      </span>
    </div>
  );
};

Something.propTypes = {
  selected: PropTypes.bool
};

export default Something;

Laying a trap

Take a moment and refresh your memory on the trap we've been laying. As noted above, it's possible to find an edge case where the bound cx func will return an unexpected global class name.

Remember also, we're explicitly binding our styles object map to the cx function. The issue is not a matter of being explicit. Within your file, using cx or classnames are equally explicit. Consider cx('something') in the example above versus classnames(styles.something). Both examples demonstrate where the class name comes from.

If you are going to be using the bound cx func, you will need to keep these traps in mind.

Note: Although the bound cx function exposes these edge cases, it's really a failing of the developer. You shouldn't be placing global classes in your CSS modules (unless you wrap them in :global()). And you shouldn't be referencing local classes in your component that aren't actually in your CSS module. Whoops!

Trap 1: an unexpected global

Consider this: cx('something-local').

If that class name isn't defined in your CSS module (remember our example CSS file above), then an unmangled global classname will be applied instead. Meaning, your <div className={cx('something-local')} /> will be rendered as <div class="something-local" />. If you were expecting <div class="__mangled__something-local" /> it could lead to unexpected style errors, especially if something-local is defined in some hidden corner of your global CSS.

Pretend your application includes Twitter Bootstrap CSS. In that case, there will be a .container class globally available. Imagine that you thought that you had a container class in your CSS module. You might be surprised when the global container CSS gets applied. In the example below, mentally replace .something-local with .container.

// Trap 1: an unexpected global
import classnames from 'classnames/bind';

import styles from './styles.css';

// whoops! something-local is undefined
styles['something-local'];  // --> undefined

const cx = classname.bind(styles);

// perhaps unepectedly, outputs a *global* class reference
cx('something-local'); // --> "something-local" not "__mangled__something-local"

Note: you need to use your imagination here. This is very likely to show up after a refactor, when a class name declaration was removed from the CSS file but not from the component. In that case, the class name will no longer exist in the styles import. If, by random chance, that old class name is defined in some globally included CSS that you are unaware of, you might accidentally run into a class name collision.

Trap 2: an unexpected local

Also, consider this: cx('something-global').

If that class name is defined in your CSS module (again, remember our example CSS file above), then a mangled classname will be used instead! This could also lead to unexpected style errors. Meaning, your <div className={cx('something-global')} /> will be rendered as <div class="__mangled__something-global" />. If you were expecting <div class="something-global" /> it could lead to unexpected style errors, especially if something-global is designed to be included from your global CSS.

Again, pretend your application includes Twitter Bootstrap CSS, and you want to use the global .container class. Imagine that someone has, unexpectedly, added a .container class to your CSS module. You might be surprised when the global styles aren't applied. In the example below, mentally replace .something-global with .container.

// Trap 2: an unexpected local
import classnames from 'classnames/bind';

import styles from './styles.css';

// whoops! something-global is actually local
styles['something-global'];  // --> __mangled__something-global

const cx = classname.bind(styles);

// perhaps unepectedly, outputs a *local* class reference
cx('something-global'); // --> "__mangled__something-global" not "something-global"

How cx works

Traps aside, it's important to look deeper into how the bound cx function works. Perhaps understanding the inner workings better will make the traps more apparent and easier to avoid.

Toy version of classnames.bind

First thing's first... the bound version of classnames isn't magic. It's a bound selector. To demonstrate the key functionality, consider this toy example where we're creating our own cx function from scratch.

You can see below some examples of what classnames, cx and cxFake return respectively. You can also see that nothing prevents you from referencing the styles object directly. It's worth noting that the classnames func imported from 'classnames/bind' works exactly the same as importing it from 'classnames' (except it now has a bind method).

import { get } from 'lodash';
import classnames from 'classnames/bind';

import styles from './styles.css';

// attempt to read the value from the styles object, return the key by default
const fakeBind = (styles) => (key) => get(styles, key, key); // <-- a curried selector

const cx = classnames.bind(styles);
const cxFake = fakeBind(styles); // <-- "binding" is another name for currying

// regular
classnames('global-style') // --> global-style
classnames('something') // --> something (global)
classnames(styles.something) // --> __mangled__something (local)

// bound
cx('global-style') // --> global-style
cx('something') // --> __mangled__something (local)
cx(styles.something) // --> __mangled__something (oh, cool)

// fake bound
cxFake('global-style') // --> global-style
cxFake('something') // --> __mangled__something (local)
cxFake(styles.something) // --> __mangled__something

Looking deeper at the fakeBind function, we can see that it is simply returning a property from the styles object. In cases where that property isn't present on the styles object, it simply returns the original string. Nothing could be simpler!

Advanced version of fakeBind

The toy example above is missing some functionality from the real classname function. For fun, here's a more complete example that implements the basics of the classnames and cx functions, including the ability to specify multiple class names.

// handles boolean class name map objects
const reduce = (styles) => (obj) => {
  return Object.keys(obj).reduce((final, key) => {
    if (!!obj[key]) { // <-- only include truthy keys
      final += ' ' + get(styles, key, key);
    }
    return final;
  }, '');
}

// joins multiple class names and boolean class name maps
// NOTE: for simplicity, this will add extra spaces
const fakeBind = (styles) => (...keys) => {
  keys.reduce((final, key) => {
    if (typeof key !== 'string') {
      final += ' ' + reduce(styles)(key); // <-- presume it's a boolean class name map
    } else {
      final += ' ' + get(styles, key, key); // <-- attempt to select the class name from the styles map
    }
    return final;
  }, '');
}

const styles = { b: '__mangled__b' }; // <-- let's create our own CSS modules styles map

const cxFake = fakeBind(styles);

cxFake('a', 'b', { c: false, d: true }); // --> a __mangled__b d

Conclusion

Hopefully this overview helps to demystify how using bound styles with classnames can help simplify your roll out of CSS modules in your React application. Although there is one important pitfall to keep in mind, the greatly reduced syntax is a big win for productivity and readability.

@christiewolters
Copy link

Thank you! The guild helped me fix some issues I was facing uplifting to a newer react version. Very helpful!

@avallonazevedo
Copy link

Fantastic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment