cn vs clsx:为什么在 Tailwind CSS 项目中我更推荐 cn

如果你使用过 shadcn/ui 或者一些高质量的 React 组件库,你很可能在 lib/utils.ts 文件中见过这样一个工具函数:

ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

这个 cn 函数看起来很简单,只是将 clsxtailwind-merge 两个库包裹了一下。那么问题来了:我们为什么需要它?直接使用 clsx 不可以吗?

要回答这个问题,我们需要先了解这两个库各自的作用。

clsx:优雅地拼接 Class

clsx 是一个非常小巧且高效的工具库,它的唯一作用就是帮助我们更方便、更优雅地拼接 CSS class 名称。在构建动态组件时,我们经常需要根据不同的状态或属性来决定是否应用某个 class,clsx 让这件事变得非常简单。

它支持多种数据类型作为参数:

可以看到,clsx 解决了条件性添加 class 的问题,代码可读性很高。但它有一个“问题”:它只负责拼接,不解决冲突。

tailwind-merge:智能地解决冲突

在 Tailwind CSS 中,样式的冲突非常常见。例如,一个组件默认有 p-4 (padding: 1rem),但我们想在某个特定场景下覆盖它,传入 p-6 (padding: 1.5rem)。

如果我们直接用 clsx 会发生什么?

js
clsx('p-4', 'p-6');
//=> 'p-4 p-6'

最终的 class 会是 p-4 p-6。这会导致最终渲染的样式取决于 CSS 文件中这两个 class 的定义顺序,这是一种不确定性,也是我们想要避免的。

tailwind-merge 就是为了解决这个问题而生的。它能够识别并理解 Tailwind 的 class,并智能地合并它们,保留最后那个有效的 class。

js
import { twMerge } from 'tailwind-merge';

twMerge('p-4 p-6');
//=> 'p-6'

twMerge('bg-red-500 bg-blue-500');
//=> 'bg-blue-500'

它会确保样式属性的唯一性,完美解决了冲突问题。

cn:强强联合,最佳实践

现在我们可以回答最初的问题了:

为什么用 cn 而不是直接使用 clsx ?

答案是:我们需要 clsx 的便利性来组织 class,也需要 tailwind-merge 的智能性来解决冲突。cn 函数将两者结合,提供了两全其美的解决方案。

让我们看一个完整的例子:

js
// 一个基础按钮组件
const Button = ({ className, ...props }) => {
  const baseClasses = 'px-4 py-2 bg-blue-500 text-white rounded';
  
  // 使用 cn 来合并基础样式和传入的自定义样式
  return <button className={cn(baseClasses, className)} {...props} />;
}

// 使用组件时,覆盖部分样式
<Button className="bg-red-500 px-6" />

在这个例子中:

  1. clsx 首先会将 baseClassesclassName 拼接成一个长字符串:'px-4 py-2 bg-blue-500 text-white rounded bg-red-500 px-6'
  2. 然后 tailwind-merge 会处理这个字符串,解决冲突:
    • px-4px-6 冲突,保留 px-6
    • bg-blue-500bg-red-500 冲突,保留 bg-red-500
  3. 最终生成的 class 是:'py-2 text-white rounded bg-red-500 px-6'

这正是我们想要的结果!通过 cn 工具函数,我们既能享受 clsx 带来的灵活组合,又能确保 Tailwind CSS 样式的可预测性和正确性,让组件的样式覆盖(override)变得安全又简单。

新故事即将发生
给VSCode添加使用标签包裹快捷键

评论区

评论加载中...