DEV Community 👩‍💻👨‍💻

Cover image for Replace clsx, classnames or classcat with your own little helper
Gustavo Guichard
Gustavo Guichard

Posted on • Updated on

Replace clsx, classnames or classcat with your own little helper

Have you ever took some time away from programming to later came back and struggle with package updates, outdated dependencies, or broken code? This happens because if your project has too many libs & packages, you might want to consider reducing the number of external imports your project has.

We experienced this recently and saw it as an opportunity to write a small helper util to replace a popular package.
Today I'll be refactoring, and improving, a popular package called clsx (also classnames and others).

Planning

With clsx you can pass a bunch of strings, objects, arrays and it'll always resolve to a string of classes to be used in your elements. If you're using something like Tailwind, where everything is done through classes, you probably rely a lot on that function.

However, my colleagues and me rarely called it with objects.
So, instead of something like this:

clsx('base', undefined, ['more', 'classes'], {
  'bg-red': hasError,
  'pointer-events-none': !isEnabled,
  'font-semibold': isTitle,
  'font-normal': !isTitle,
})

// Result: "base more classes bg-red font-normal"
Enter fullscreen mode Exit fullscreen mode

We'd rather have an API like:

cx('base', undefined, ['more', 'classes'],
  hasError && 'bg-red',
  isEnabled || 'pointer-events-none',
  isTitle ? 'font-semibold' : 'font-normal'
)

// Result: "base more classes bg-red font-normal"
Enter fullscreen mode Exit fullscreen mode

Actually, with the addition of the || operator, the end API turned out to be better for our needs.

The implementation

It is a good practice to always start by modeling the types:

type Cx = (...a: Array<undefined | null | string | boolean>) => string
Enter fullscreen mode Exit fullscreen mode

So basically we need to accept strings, nullish values and booleans and then strip them out (including true so we can take advantage of the || operator)

This project heavily uses lodash so we've used it to compose the function:

import { compose, join, filter, isBoolean, isNil, flatten } from 'lodash/fp'

const cx: Cx = (...args) => 
  compose(join(' '), filter(isBoolean), filter(isNil), flatten)(args)
Enter fullscreen mode Exit fullscreen mode

And of course, as I said in the start of this post, if you don't like to be adding packages for everything you'll want the vanilla version:

const cx: Cx = (...args) =>
  args
    .flat()
    .filter(x => 
      x !== null && x !== undefined && typeof x !== 'boolean'
    ).join(' ')
Enter fullscreen mode Exit fullscreen mode

Conclusion

Think twice before adding yet another package. Sometimes everything you need is couple lines of code - which is less than what goes to your package-lock.json at the end of the day.

Top comments (0)

👋 Every week new members join DEV and share a bit about them in our Welcome Thread

Welcome them to DEV and share a bit about yourself