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"
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"
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
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)
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(' ')
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 (5)
It would better be replaced with something like this, that handles objects, arrays, and everything you would pass to to function
Actually nowadays I'm leaning towards this:
Simplicity - which is the goal of this post - and I don't think it should have several APIs. I also don't like the object API as we usually have a bunch of classes - especially when using tailwind - as keys of object.
Yes you are right, I understand. It might be better sometimes to not use objects as you wrote it. Thanks :)
Doesn't the inclusion of lodash defeat the purpose here?
By the time I wrote this post I ise to have lodash in pretty much every project. That is not the case anymore so I’ve been going with the vanilla approach.
Actually nowadays I’m just doing:
I guess I should update the post 😜