Opensourcing classd: A fast and modern classNames alternative
Ganesh Prasad
Posted on September 3, 2019
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:
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:
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,
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.
Strings and numbers are valid values and are added to the output.
It drops undefined, null, NaN and boolean values.
If the value is an Array or an Iterable, it flattens the value and recursively processes the elements.
If the value is an Object or a Map, it drops keys associated with falsy values and adds the remaining keys to the output.
If the value is a function, it calls the function and adds its return value if that's valid
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:
classd (Tag for template literals, default)
classDedupe (Tag for template literals)
classdFn (Variadic function, for compatibility, similar to classNames)
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)importclassdfrom'classd';// example useconstwidth=1080;constclasses=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 functionsimport{classd,classDedupe,classdFn,classDedupeFn}from'classd';// example use (of classdFn)constwidth=1080;constclasses=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)constclassd=require('classd').default// commonjs require any of the exported functionsconst{classd,classDedupe,classdFn,classDedupeFn}=require('classd');// commonjs require classd moduleconstclassd=require('classd');// exports can be used as classd.classd, classd.classDedupe etc
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:
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.
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.
A fast and minimal ES6 utility to conditionally compose classnames
classd
A minimal ES6 utility to compose classnames
classd is a fast, minimal JavaScript(ES6) utility for composing class names
It builds on ideas and philosophy similar to that of JedWatson's classnamesclassd 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:
classd (Tag for template literals, default)
classDedupe (Tag for template literals)
classdFn (Variadic function, for compatibility, similar to classNames)
classDedupeFn (Variadic function, for compatibility, similar to classNames/dedupe)
# 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