Bisexual Pride

Last week, one of my apprentices asked me how to conditionally add a class to an element in React. For example, consider a <Cart /> component that is styled differently depending on whether or not it has products. The component always has a .cart class, but it only conditionally has a .cart-hasProducts class (which follows the MaintainableCSS convention for stateful classes).

Basic approaches

Perhaps the simplest approach is to use string concatenation. Start with the classes you always want to be present, then conditionally add space-separated classes.

function Cart(props) {
const [products, setProducts] = useState(props.products);
const hasProducts = products.length > 0;

let classNames = "cart";

if (hasProducts) {
classNames += " cart-hasProducts";
}

return <div className={classNames}></div>;
}

You can achieve the same result using an array and the Array.prototype.join() method. I like this approach because you only need to remember the spaces once—when you call the .join() method—not every time you concatenate a class name.

function Cart(props) {
const [products, setProducts] = useState(props.products);
const hasProducts = products.length > 0;

const classNames = ["cart"];

if (hasProducts) {
classNames.push("cart-hasProducts");
}

return <div className={classNames.join(" ")}></div>;
}

My apprentice and I used a template literal and the conditional (ternary) operator. If the cart has products, we interpolate the "cart-hasProducts" string, otherwise we interpolate an empty string. This is nice because it’s succinct, but it feels a bit messy. I also dislike interpolating an empty string just because an operand is required.

function Cart(props) {
const [products, setProducts] = useState(props.products);
const hasProducts = products.length > 0;

return (
<div className={`cart ${hasProducts ? "cart-hasProducts" : ""}`}></div>
);
}

An npm package

After a quick Google search, we discovered an npm package called classnames. Here’s an example from the documentation.

The classNames function takes any number of arguments which can be a string or object. The argument 'foo' is short for { foo: true }. If the value associated with a given key is falsy, that key won’t be included in the output.

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'

The .cart class should always be present, so we can add it using a string argument. The .cart-hasProducts class should only conditionally be present, so we can add it using an object.

function Cart(props) {
const [products, setProducts] = useState(props.products);
const hasProducts = products.length > 0;

const cartClassNames = classNames("cart", {
"cart-hasProducts": hasProducts,
});

return <div className={cartClassNames}></div>;
}

A simple function

The classnames package is great, but I don’t like to install dependencies for things I can easily achieve myself. It’s not about reinventing the wheel, it’s about reducing technical debt and improving security.

I decided to write a simple helper function. All class names are added using an object. If a value is truthy, its key is used. If a value is falsy, its key is not used.

function getClassNames(classes = {}) {
return Object.keys(classes)
.filter(className => classes[className])
.join(" ");
}

The function has a single parameter, classes, whose default value is an empty object. It does three things:

  1. Uses the Object.keys() method to get an array of the object’s keys.
  2. Uses the Array.prototype.filter() method to filter out the keys whose values are falsy.
  3. Uses the Array.prototype.join() method to join the filtered keys with spaces.

The .cart class should always be present, so the value of the object’s cart property is true. The .cart-hasProducts class should only conditionally be present, so the value of the object’s cart-hasProducts property is computed (true if products.length > 0, otherwise false).

function Cart(props) {
const [products, setProducts] = useState(props.products);
const hasProducts = products.length > 0;

const classNames = getClassNames({
cart: true,
"cart-hasProducts": hasProducts,
});

return <div className={classNames}></div>;
}

The helper function is free to use under the terms of the MIT License.