DEV Community

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

Replace clsx, classnames or classcat with your own little helper

Gustavo Guichard on August 18, 2021

Have you ever taken a break from programming, only to return and wrestle with package updates, outdated dependencies, or broken code? This issue of...
Collapse
 
sarajohn130 profile image
sara john • Edited

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, namely true && 'bar'

Collapse
 
gugaguichard profile image
Gustavo Guichard

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 libs

Collapse
 
sarajohn130 profile image
sara john

Doing !isEnabled && 'pointer-events-none' or isDisabled && 'pointer-events-none' is equivalent so that's kind of a nitpick.

Thread Thread
 
gugaguichard profile image
Gustavo Guichard

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.

Thread Thread
 
sarajohn130 profile image
sara john • Edited

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.

Thread Thread
 
gugaguichard profile image
Gustavo Guichard

Sure, I mean the following API is weird:

clsx('bg-white rounded shadow', {
  'px-4 py-3 bg-slate-50 shadow-lg': isHovering,
})
Enter fullscreen mode Exit fullscreen mode

Got it?

Collapse
 
sarajohn130 profile image
sara john • Edited

In your example

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

what is the point of undefined and 'base?' Shouldn't 'base' be in ['more', 'classes']?

The proper example seems to look like this:

clsx(['base', 'more', 'classes'], {
  'bg-red': hasError,
  'pointer-events-none': !isEnabled,
  'font-semibold': isTitle,
  'font-normal': !isTitle,
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
gugaguichard profile image
Gustavo Guichard

maybe I was not as clear but I'm showing that both clsx or the solution here will concat, flatten, and filter classes.

Collapse
 
dbenfouzari profile image
Donovan BENFOUZARI

It would better be replaced with something like this, that handles objects, arrays, and everything you would pass to to function

type StringArg = string | number | null | undefined;
type ObjArg = Record<string, any>;
type ArrArg = Array<StringArg | ObjArg | ArrArg>;
export type ClsxArgs = Array<StringArg | ObjArg | ArrArg>;

export function clsx(...args: ClsxArgs) {
  return args
    .reduce<Array<string | number>>((previousValue, currentValue) => {
      if (!currentValue) return previousValue;

      if (Array.isArray(currentValue)) {
        previousValue.concat(clsx(...currentValue));
        return previousValue;
      }

      if (typeof currentValue === "object") {
        Object.entries(currentValue).forEach(([k, v]) => {
          if (v) previousValue.push(k);
        });
        return previousValue;
      }

      previousValue.push(currentValue);
      return previousValue;
    }, [])
    .join(" ");
}

Enter fullscreen mode Exit fullscreen mode
Collapse
 
gugaguichard profile image
Gustavo Guichard

Actually nowadays I'm leaning towards this:

function cn(...args: Array<undefined | null | string | boolean>): string {
  return args
    .flat()
    .filter(x => typeof x === "string")
    .join(" ");
}
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
dbenfouzari profile image
Donovan BENFOUZARI

Yes you are right, I understand. It might be better sometimes to not use objects as you wrote it. Thanks :)

Collapse
 
sarajohn130 profile image
sara john

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?

Collapse
 
gugaguichard profile image
Gustavo Guichard

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?

Collapse
 
sarajohn130 profile image
sara john

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.

Collapse
 
joenano profile image
joenano

Doesn't the inclusion of lodash defeat the purpose here?

Collapse
 
gugaguichard profile image
Gustavo Guichard

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:

function cx(args: unknown[]) {
  return args
    .flat()
    .filter(x => typeof x === 'string')
    .join(' ')
    .trim()
}
Enter fullscreen mode Exit fullscreen mode

I guess I should update the post 😜

Collapse
 
sarajohn130 profile image
sara john

You should update it now

Thread Thread
 
gugaguichard profile image
Gustavo Guichard

Updated it, thx