DEV Community

loading...
Cover image for Opensourcing classd: A fast and modern classNames alternative

Opensourcing classd: A fast and modern classNames alternative

gnsp profile image Ganesh Prasad ・6 min read

If you are a frontend developer, there is a fair chance that you have used
Jedwatson's classNames package in your projects. In fact, classNames is the official replacement for classSet, which was originally shipped in the React.js Addons bundle. It's one of the most used packages in the world of React.

A simple usecase for classNames

For instance, if we needed to conditionally apply css classes to an element inside a React component based on the component state, we may do it like the following:

class EnhancedButton extends React.Component {
    // ...

    render () {
        const btnClass = classNames({
            'btn': true,
            'btn-large': true,
            'btn-primary': true,
            'btn-pressed': this.state.isPressed,
            'btn-over': !this.state.isPressed && this.state.isHovered
        });

        return <button className={btnClass}>{this.props.label}</button>;
    }
}

The approach above is neat, but given that many of the classes are applied unconditionally, the pattern of setting them to true in the configuration object becomes rather redundant. To counter that redundancy, we may write something like the following:

const btnClass = classNames(
    'btn', 
    'btn-large',
    'btn-primary',
    {
        'btn-pressed': this.state.isPressed,
        'btn-over': !this.state.isPressed && this.state.isHovered  
    }
);

Age of ES6 template literals

However, as ES6 or ES2015 template literals became supported across mainstream browsers, it became simpler to write the above snippet as:

const btnClass = `
    btn 
    btn-large 
    btn-primary 
    ${this.state.isPressed ? 'btn-pressed' : ''}
    ${(!this.state.isPressed && this.state.isHovered) ? 'btn-over' : ''}
`;

Though the template literal way of writing class names is somewhat simpler and faster, it's still fairly redundant and it's not free of certain pitfalls. For example, while writing ${(!this.state.isPressed && this.state.isHovered) ? 'btn-over' : ''}, making sure that an empty string '' gets added if the condition fails, is quite redundant and long. And the template literal does not remove extra/unnecessary whitespace and newlines from the output on its own.

For example, why not write something like ${!this.state.isPressed && this.state.isHovered && 'btn-over'} ? But there is a pitfall; if the condition resolves to true, the btn-over gets added, but if the condition resolves to false, the string 'false' gets added to the output. What if we write expressions that resolve to undefined or null or NaN or anything similar ? Javascript would simply treat them as strings and move on. It goes without saying that there are plenty of such 'shoot in the foot' scenarios with this approach.

Moreover, what if we already have a valid configuration object and we want to reduce it to a classnames string ? There is no obvious way to do that directly using only the template literals, we may possibly do it like Object.keys(config).reduce(...), or we may use the classNames package to do it for us. Of course, using the classNames package is more performant, because the package is well optimized for this usecase.

But what if there was a way to write the above example like the following without having any behavioral pitfalls and without losing any performance,

const btnClass = secretSauce`
    btn
    btn-large
    btn-primary
    ${this.state.isPressed && 'btn-pressed'}
    ${!this.state.isPressed && this.state.isHovered && 'btn-over'}
`;

Enter classd

classd is the secretSauce you needed in the example above. It's tagged template based fast and modern classNames alternative that preserves all the awesome bits of classNames and augments it with more.

The classd tag processes the interpolation values in the template literal according to the following specification.

  1. Strings and numbers are valid values and are added to the output.
  2. It drops undefined, null, NaN and boolean values.
  3. If the value is an Array or an Iterable, it flattens the value and recursively processes the elements.
  4. If the value is an Object or a Map, it drops keys associated with falsy values and adds the remaining keys to the output.
  5. If the value is a function, it calls the function and adds its return value if that's valid
  6. It removes all unnecessary whitespace.

Here are a few examples:

classd`foo bar`; // => 'foo bar'
classd`foo ${null && 'bar'}`; // => 'foo'
classd`foo-${true && 'bar'}`; // => 'foo-bar'
classd`${true} ${false}`; // => ''
classd`${{ foo: true, bar: false}}`; // => 'foo'
classd`${{foo: true}} ${{bar: true}} ${{baz: false}}`; // => 'foo bar'
classd`a ${[ 'b', 'c', false && 'd' ]}`; // => 'a b c'
classd`${['a', { b: 1, c: 0 }]}`; // 'a b'
classd`    a    b  \n  ${Array(10).fill(' ')} c`; // => 'a b c'

Installation and usage

The classd package exports 4 functions:

  1. classd (Tag for template literals, default)
  2. classDedupe (Tag for template literals)
  3. classdFn (Variadic function, for compatibility, similar to classNames)
  4. classDedupeFn (Variadic function, for compatibility, similar to classNames/dedupe)

The package is available on NPM can be installed using package managers like npm and yarn. It can also be pulled from CDN directly into your webpages.

Install using package manager

# via npm 
npm install --save classd

# or Yarn 
yarn add classd

Use in ES6 Modules


// ES6 import (default - classd tag for template literals)
import classd from 'classd';

// example use
const width = 1080;
const classes = classd`container padding-${{
    lg: width > 1280, 
    md: width > 960 && width < 1280,
    sm: width <= 960
}} margin-0 ${width > 960 && 'blue'} ${width < 960 && 'red'}`;
console.log(classes); // => 'container padding-md margin-0 blue'


// ES6 import any of the exported functions
import { classd, classDedupe, classdFn, classDedupeFn } from 'classd';

// example use (of classdFn)
const width = 1080;
const classes = classdFn ('container', {
    'padding-lg': width > 1280, 
    'padding-md': width > 960 && width < 1280,
    'padding-sm': width <= 960
}, (width > 960 && 'blue'), 'margin-0');
console.log(classes); // => 'container padding-md blue margin-0'

Use in Commonjs modules (Nodejs)

// commonjs require classd tag for template literals (default export)
const classd = require('classd').default

// commonjs require any of the exported functions
const { classd, classDedupe, classdFn, classDedupeFn } = require('classd');

// commonjs require classd module
const classd = require('classd'); // exports can be used as classd.classd, classd.classDedupe etc

Pull from CDN


<script src='https://cdn.jsdelivr.net/npm/classd@1.0/lib/index.js'></script>
<script type='text/javascript'>
    const { classd, classDedupe, classdFn, classDedupeFn } = window.classd;
    console.log(classd`container ${1 > 0 && 'blue'}`); // => 'container blue'
</script> 

Well, what are classDedupe, classdFn and classDedupeFn ?

The classdFn follows the same specifications as the classd tag. It's a straightforward replacement for classNames. Everything that's valid with classNames is also valid with classdFn. In addition, classdFn supports passing Maps, Sets, and other Iterables as arguments. Moreover it's slightly faster than classNames in general usage.

If you want to migrate an existing project from using classNames to classd, using the classdFn is the fastest and simplest thing to do. The migration from classNames is as simple as:

// before
import classNames from 'classnames';

//after
import { classdFn as classNames } from 'classd';

The classDedupe tag is an enhanced and about 60% slower version of the classd tag. It does everything that the classd tag does. In addition to that it checks for repeating names among the class names and ensures that each valid class name appears only once in the output string.

The classDedupeFn is the function equivalent of the classDedupe tag. It follows the same signature as classdFn and classNames.

It differs from the classNames/dedupe in the behaviour that, the classNames/dedupe unsets a class if a configuration object appearing later in its arguments unsets it; whereas classDedupe does not unset a class name once it's set.

What about performance and stability ?

As conditionally applying class names is a common task in web frontend, and the functions are supposed to be called many times during a render cycle, it's imperative that the implementation of classd be highly performant and stable. Therefore we take the stability and performance of this package very seriously. Updates are thoroughly reviewed for performance impacts before being released. We maintain a comprehensive test suite to ensure stability.

Here is a JSPerf benchmark of the classd package, compared against classNames. As we can see, the classd tag is as performant as classNames, while the classdFn is slightly faster.

JSPerf benchmark

Source code and contributing

The source code is available on Github for you. Any contributions in the form of Pull Request, Issue or Suggestion are welcome. If you like it, please give it a star on Github.

GitHub logo GnsP / classd

A fast and minimal ES6 utility to conditionally compose classnames

classd

A minimal ES6 utility to compose classnames

NPM version NPM Weekly Downloads License

classd is a fast, minimal JavaScript(ES6) utility for composing class names It builds on ideas and philosophy similar to that of JedWatson's classnames classd defaults to the idea of using ES6 template literals for composing class names. It also provides functions similar to classNames and classNames/dedupe for compatibility (with a minor behavioural difference in case of classNames/dedupe detailed in a subsequent section).

It exports 4 functions:

  1. classd (Tag for template literals, default)
  2. classDedupe (Tag for template literals)
  3. classdFn (Variadic function, for compatibility, similar to classNames)
  4. classDedupeFn (Variadic function, for compatibility, similar to classNames/dedupe)

Installation

Install with npm, or Yarn:

# via npm
npm install --save classd

# or Yarn (note that it will automatically save the package to your `dependencies` in `package.json`)
yarn add classd

Use with ES6 modules (import)

// IMPORTING IN ES6
///////////////////
//

Thanks for reading and do give it a try !

Discussion (0)

pic
Editor guide