Have you ever taken a break from programming, only to return and wrestle with package updates, outdated dependencies, or broken code? This issue often arises if your project heavily relies on a multitude of libraries and packages. In such instances, you might want to consider reducing the number of external imports your project utilizes.
We encountered this issue recently and perceived it as an opportunity to write a small helper utility that could replace a popular package.
Today, I'll be replacing a widely-used package called clsx
(or classnames, etc) with a tiny function.
Planning
With clsx
, you can pass a mix of strings, objects, arrays, and it consistently resolves to a string of classes to be used in your elements. If you're using a framework like Tailwind, where everything is accomplished through classes, you likely depend heavily on this function.
However, my colleagues and I rarely used it with objects.
Instead of 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 prefer 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"
In fact, with the addition of the || operator, the final API proved to be even better suited for our needs.
The implementation
The final version of our function, which is just a few lines of code, accepts an array of unknown values, filters for strings, joins them with a space, and trims any leading or trailing whitespace:
function cx(...args: unknown[]) {
return args
.flat()
.filter(x => typeof x === 'string')
.join(' ')
.trim()
}
Conclusion
Think twice before adding yet another package. Sometimes, all you need are a few lines of code - which might be fewer than the additions made to your package-lock.json at the end of the day. Always consider whether you can achieve the same functionality with a simpler, custom solution.
Top comments (18)
When you say "We prefer an API like:", and your helper does
hasError && 'bg-red'
, doesn't clsx also support this style? On the npm page npmjs.com/package/clsx, I see an example that uses your style, namelytrue && 'bar'
it means we don't care about the object-like API, especially because with a tech like Tailwind the keys of the object get too long and it doesn't feel right.
But also we like to be able to do:
isEnabled || 'pointer-events-none'
and the||
last time I checked was not supported by those libsDoing
!isEnabled && 'pointer-events-none'
orisDisabled && 'pointer-events-none'
is equivalent so that's kind of a nitpick.Yes, I’m not saying it is not possible to do it otherwise. I’m just saying that I’d rather build my own simple API that works the way I want so it can have these nuances.
I’m trying to understand if you are advocating for using clsx or what. I’m not against it at all, my whole point is that sometimes you can replace a library with one line of code. Ignoring that can lead to the isEven package situation.
Fair point. Might as well replace a library if it can be replicated with just your 5 lines of code. I'm not for or against clsx. I originally thought you were advocating for better syntax if you wrote your own clsx, but looking into it the difference is not significant between the 2. But now I believe syntax is not the main point of this post but instead it's to avoid the isEven scenario?
Also in your previous comment when you said "it means we don't care about the object-like API, especially because with a tech like Tailwind the keys of the object get too long and it doesn't feel right.", could you elaborate on that? I did not understand.
Sure, I mean the following API is weird:
Got it?
In your example
what is the point of undefined and 'base?' Shouldn't 'base' be in ['more', 'classes']?
The proper example seems to look like this:
maybe I was not as clear but I'm showing that both clsx or the solution here will concat, flatten, and filter classes.
When you said "We encountered this issue recently and perceived it as an opportunity to write a small helper utility that could replace a popular package.", could you elaborate on it? What package was it and how did you solve it?
it had to do with the previous package: coming back from vacations and there's a lot of package updates to do... but I'm not attached to that sentence, do you want to suggest something else?
Nope, just curious how updating packages went for you. I read from someone online that for the past 2 years their team started using a serverless framework they made that allowed them to ship very fast. And they don't have to care about dependencies anymore.
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 😜
You should update it now
Updated it, thx