やかんです。
突然ですが、tailwind-mergeというものがあります。
https://github.com/dcastil/tailwind-merge
↑こちらですね。tailwind-cssのクラス名について、まるっとよしなにやってくれる便利ライブラリなんですが、挙動が気になったので備忘録的にまとめます。
tailwind-mergeとは。
https://github.com/dcastil/tailwind-merge
↑これです。tailwind cssのユーティル関数で、クラス名の競合周りをまるっと解決してくれます。
仕組みを理解する。
OSSなので、コードの掲載は特に問題ないんじゃないかなって思ってます。問題がある場合は教えてください。
twMerge
は/src/lib/tw-merge.ts
で以下のように定義されています。
import { createTailwindMerge } from './create-tailwind-merge'
import { getDefaultConfig } from './default-config'
export const twMerge = createTailwindMerge(getDefaultConfig)
となると、createTailwindMerge
, getDefaultConfig
の理解が必要ですね。twMerge
が関数なので、createTailwindMerge
は「関数を返す関数」であってほしいです、定義を見てみましょう。

「関数を返す関数」のことを「高階関数」と呼びます。
import { createConfigUtils } from './config-utils'
import { mergeClassList } from './merge-classlist'
import { ClassNameValue, twJoin } from './tw-join'
import { AnyConfig } from './types'
type CreateConfigFirst = () => AnyConfig
type CreateConfigSubsequent = (config: AnyConfig) => AnyConfig
type TailwindMerge = (...classLists: ClassNameValue[]) => string
type ConfigUtils = ReturnType<typeof createConfigUtils>
export function createTailwindMerge(
createConfigFirst: CreateConfigFirst,
...createConfigRest: CreateConfigSubsequent[]
): TailwindMerge {
let configUtils: ConfigUtils
let cacheGet: ConfigUtils['cache']['get']
let cacheSet: ConfigUtils['cache']['set']
let functionToCall = initTailwindMerge
function initTailwindMerge(classList: string) {
const config = createConfigRest.reduce(
(previousConfig, createConfigCurrent) => createConfigCurrent(previousConfig),
createConfigFirst() as AnyConfig,
)
configUtils = createConfigUtils(config)
cacheGet = configUtils.cache.get
cacheSet = configUtils.cache.set
functionToCall = tailwindMerge
return tailwindMerge(classList)
}
function tailwindMerge(classList: string) {
const cachedResult = cacheGet(classList)
if (cachedResult) {
return cachedResult
}
const result = mergeClassList(classList, configUtils)
cacheSet(classList, result)
return result
}
return function callTailwindMerge() {
return functionToCall(twJoin.apply(null, arguments as any))
}
}
↑こちらが、/src/lib/create-tailwind-merge.ts
です。所望のcreateTailwindMerge
が定義されています。引数にも着目しますが、まずは返り値に着目します。
return function callTailwindMerge() {
return functionToCall(twJoin.apply(null, arguments as any))
}
関数が返されています。functionToCall
という関数は、29行目で以下のように与えられています。
functionToCall = tailwindMerge
では、右辺のtailwindMerge
とは何か、と言うと、34行目から定義されていますね。
function tailwindMerge(classList: string) {
const cachedResult = cacheGet(classList)
if (cachedResult) {
return cachedResult
}
const result = mergeClassList(classList, configUtils)
cacheSet(classList, result)
return result
}
この関数を読むと、twMerge
がまずキャッシュを見ていることがわかります。天下り的で恐縮ですが、ここで言うところののキャッシュはブラウザのキャッシュじゃないです。プログラムのメモリ上に確保されるデータで、そのためSSRでもCSRでもtwMerge
は使えるという話だと思います。プログラム上のメモリなので、CSRの場合画面をリロードするとこの「キャッシュ」は消えます。
で、この関数の返り値はresult
でして、以下のように定義されています。
const result = mergeClassList(classList, configUtils)
その下の行でこのresult
がcache(プログラム上のメモリ)に保存されたりしてますが、一旦キャッシュ周辺は置いといて、気になるのはmergeClassList
ですね。mergeClassList
は/src/lib/merge-classlist.ts
で以下のように定義されています。
import { ConfigUtils } from './config-utils'
import { IMPORTANT_MODIFIER } from './parse-class-name'
const SPLIT_CLASSES_REGEX = /\s+/
export const mergeClassList = (classList: string, configUtils: ConfigUtils) => {
const { parseClassName, getClassGroupId, getConflictingClassGroupIds, sortModifiers } =
configUtils
/**
* Set of classGroupIds in following format:
* `{importantModifier}{variantModifiers}{classGroupId}`
* @example 'float'
* @example 'hover:focus:bg-color'
* @example 'md:!pr'
*/
const classGroupsInConflict: string[] = []
const classNames = classList.trim().split(SPLIT_CLASSES_REGEX)
let result = ''
for (let index = classNames.length - 1; index >= 0; index -= 1) {
const originalClassName = classNames[index]!
const {
isExternal,
modifiers,
hasImportantModifier,
baseClassName,
maybePostfixModifierPosition,
} = parseClassName(originalClassName)
if (isExternal) {
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
let hasPostfixModifier = !!maybePostfixModifierPosition
let classGroupId = getClassGroupId(
hasPostfixModifier
? baseClassName.substring(0, maybePostfixModifierPosition)
: baseClassName,
)
if (!classGroupId) {
if (!hasPostfixModifier) {
// Not a Tailwind class
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
classGroupId = getClassGroupId(baseClassName)
if (!classGroupId) {
// Not a Tailwind class
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
hasPostfixModifier = false
}
const variantModifier = sortModifiers(modifiers).join(':')
const modifierId = hasImportantModifier
? variantModifier + IMPORTANT_MODIFIER
: variantModifier
const classId = modifierId + classGroupId
if (classGroupsInConflict.includes(classId)) {
// Tailwind class omitted due to conflict
continue
}
classGroupsInConflict.push(classId)
const conflictGroups = getConflictingClassGroupIds(classGroupId, hasPostfixModifier)
for (let i = 0; i < conflictGroups.length; ++i) {
const group = conflictGroups[i]!
classGroupsInConflict.push(modifierId + group)
}
// Tailwind class not in conflict
result = originalClassName + (result.length > 0 ? ' ' + result : result)
}
return result
}
mergeClassList
の返り値はresult
という変数で、これは20行目でletで定義されています。letで定義されているところからも、「ああ、今からパースするのね」という雰囲気が伝わってきます。なわけで以下のコードが大事になるはず。
for (let index = classNames.length - 1; index >= 0; index -= 1) {
const originalClassName = classNames[index]!
const {
isExternal,
modifiers,
hasImportantModifier,
baseClassName,
maybePostfixModifierPosition,
} = parseClassName(originalClassName)
if (isExternal) {
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
let hasPostfixModifier = !!maybePostfixModifierPosition
let classGroupId = getClassGroupId(
hasPostfixModifier
? baseClassName.substring(0, maybePostfixModifierPosition)
: baseClassName,
)
if (!classGroupId) {
if (!hasPostfixModifier) {
// Not a Tailwind class
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
classGroupId = getClassGroupId(baseClassName)
if (!classGroupId) {
// Not a Tailwind class
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
hasPostfixModifier = false
}
const variantModifier = sortModifiers(modifiers).join(':')
const modifierId = hasImportantModifier
? variantModifier + IMPORTANT_MODIFIER
: variantModifier
const classId = modifierId + classGroupId
if (classGroupsInConflict.includes(classId)) {
// Tailwind class omitted due to conflict
continue
}
classGroupsInConflict.push(classId)
const conflictGroups = getConflictingClassGroupIds(classGroupId, hasPostfixModifier)
for (let i = 0; i < conflictGroups.length; ++i) {
const group = conflictGroups[i]!
classGroupsInConflict.push(modifierId + group)
}
// Tailwind class not in conflict
result = originalClassName + (result.length > 0 ? ' ' + result : result)
}
このfor文は、classNames
という事前に定義された変数を起点にループされます。このclassNames
というのは18行で定義されていて、
const classNames = classList.trim().split(SPLIT_CLASSES_REGEX)
SPLIT_CLASSES_REGEX
は4行目で/\s+/
が与えられています。これはスペースの正規表現なので、classNames
はスペースでclassList
(これはmergeClassList
の第一引数、つまりtailwind cssの文字列)を区切ったものということになります。名前の通り、「classNameたち」ですね。
このclassNames
についてループを回し、当該className
をoriginalClassName
という変数に入れます。丁寧な命名でわかりやすいです。
parseClassName
といういかにもクラス名をパースしてくれそうな関数にこのoriginalClassName
を噛ませたあと、その返り値を元にコードが実行されます。
まずこちら。
if (isExternal) {
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
再度天下り的で恐縮ですが、isExternal
というフラグは「tailwindcssのクラス名として有効か否か」を表しています。trueの場合は「externalなクラス名だよ」ということで(独自にcssファイルなどで与えているクラス名など)、特に何もされずにcontinueを迎えます。
次に、以下のコードですね。
let hasPostfixModifier = !!maybePostfixModifierPosition
let classGroupId = getClassGroupId(
hasPostfixModifier
? baseClassName.substring(0, maybePostfixModifierPosition)
: baseClassName,
)
if (!classGroupId) {
if (!hasPostfixModifier) {
// Not a Tailwind class
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
classGroupId = getClassGroupId(baseClassName)
if (!classGroupId) {
// Not a Tailwind class
result = originalClassName + (result.length > 0 ? ' ' + result : result)
continue
}
hasPostfixModifier = false
}
これは、いろいろややこしいので一旦スキップしてしまっていいかと思います。天下り的に述べると、「/
(スラッシュ)が含まれるクラス名についていい感じに処理する」というコードになっているかと思います。widthとかですね。
…
めちゃんこキリが悪いですが、一身上の都合でこの記事一旦終わります。続き書きます!
最後までお読みいただき、ありがとうございます。