はじめに
Reactでコンポーネントを書いていると、こんなコードに出会ったことはないでしょうか。
<button
className={`btn ${isActive ? 'btn-active' : ''} ${isDisabled ? 'btn-disabled' : ''} ${size === 'large' ? 'btn-large' : ''}`}
>
テンプレートリテラルの中に三項演算子が並び、余分な半角スペースが混入していないか目を凝らして確認する。条件が増えるたびにこの一行はどんどん読みにくくなっていきます。
classnamesは、この「条件付きclassName」問題をたった1つの関数呼び出しで解決してくれる、非常に小さなJavaScriptユーティリティです。真偽値・文字列・オブジェクト・配列を渡すだけで、有効なクラス名だけをスペース区切りで結合してくれます。
とはいえ、読むより触った方が早いと思います。真偽値の切り替えでクラス名がどう変わるかをその場で確認できるので、先に挙動を見たい方はこちらからどうぞ。
classnamesとは
classnamesは、Jed Watson氏が公開している、条件に応じてCSSのクラス名を組み立てるための軽量ユーティリティです。2015年の公開以来、React界隈を中心に幅広く使われ続けており、依存ゼロ・数百バイトという軽さも支持されている理由の一つです。
主な特徴
- 依存関係ゼロ - 外部パッケージに依存しない単一ファイルの実装で、バンドルサイズへの影響を最小限に抑えられます
- 柔軟な引数 - 文字列・オブジェクト・配列・真偽値を自由に混在させて渡せます
- falsyな値を自動で無視 -
false・null・undefined・0・''は結合結果から除外されるため、三項演算子を書かずに済みます - TypeScript対応 - 型定義が同梱されており、追加のインストールなしで型の恩恵を受けられます
インストール
npmを使う場合は次のコマンドでインストールできます。
npm install classnames
yarnを使う場合は以下です。
yarn add classnames
classnamesのサンプルを動かす
下のサンプルは、classnamesのデフォルトエクスポート関数(慣習的にcxという別名で使われることも多い関数)に、文字列とオブジェクトを混在させて渡す例です。チェックボックスを操作すると、オブジェクトのキーに対応するクラス名が結合結果に出たり消えたりします。
要点となる書き方はこちらです。
import classNames from 'classnames'
const isActive = true
const isDisabled = false
const result = classNames('btn', {
'btn-active': isActive,
'btn-disabled': isDisabled,
})
// => "btn btn-active"
実際に動かせるものが下です。チェックボックスの状態を切り替えて、結合結果の文字列がどう変化するか確認してみてください。
classNames()はオブジェクトの各キーのうち、値がtrueのものだけをクラス名として採用します。すべてのチェックを外すと結合結果は"btn"だけになり、btn-active・btn-disabled・btn-largeは自動的に除外されることが確認できるはずです。
基本的な使い方
もっとも単純な使い方は、複数の文字列を渡して結合するだけです。
import classNames from 'classnames'
classNames('foo', 'bar') // => 'foo bar'
真偽値によって出し分けたい場合は、オブジェクトの値に条件式を渡します。
classNames('btn', { 'btn-primary': true, 'btn-disabled': false })
// => 'btn btn-primary'
配列を渡すこともでき、ネストした配列や条件式の組み合わせも自動的にフラット化されます。
classNames(['btn', 'btn-large'], { 'btn-disabled': isDisabled })
実践的なユースケース
Reactコンポーネントの状態に応じたスタイル切り替え
Reactでボタンやカードなどのコンポーネントを作るとき、propsで受け取った状態(選択中・無効化・エラーなど)に応じてクラス名を切り替える場面は非常によくあります。classnamesを使うと、三項演算子やテンプレートリテラルを重ねずに、状態とクラス名の対応関係を一目で見渡せます。
<div id="app"></div>
タブをクリックするたびにisSelectedが切り替わり、classNames('tab', { 'tab-selected': isSelected })が返す文字列も追随して変化します。状態を管理するuseStateとクラス名の組み立てを分離できるため、見た目のロジックが読みやすくなります。
CSS Modulesと組み合わせたスコープ付きクラスの結合
CSS Modulesを使うプロジェクトでは、styles.buttonのようにハッシュ化されたクラス名をJavaScript側で条件付き結合する必要があります。classnamesはオブジェクトのキーに動的な値(CSS Modulesのインポート結果)をそのまま使えるため、この用途とも相性が良好です。ここではCSS Modulesの代わりに、同様の発想でオブジェクトのキーに変数を使う書き方を再現しています。
// CSS Modulesのインポートを模した例
const styles = { button: 'button_a1b2c3', active: 'active_x9y8z7' }
const className = classNames(styles.button, {
[styles.active]: isActive,
})
[styles.active]: isActiveのように、算出プロパティ名を使ってオブジェクトのキーに動的な値を渡せるのがポイントです。CSS Modulesが生成するハッシュ付きのクラス名でも、通常の文字列と同じ感覚でclassNamesに条件を渡せます。
配列とネストによる複数条件のグループ化
クラス名の条件が多くなってくると、1つのオブジェクトにすべて詰め込むより、意味のあるグループごとに配列でまとめたほうが見通しが良くなることがあります。classnamesは配列・オブジェクト・文字列を任意にネストしても自動的にフラット化して結合してくれます。
const sizeClasses = ['btn-large', 'btn-rounded']
const stateClasses = { 'btn-active': isActive, 'btn-disabled': isDisabled }
classNames('btn', sizeClasses, stateClasses)
配列の要素にfalseが混じっていても(roundedEl.checked && 'btn-rounded'がfalseを返すケース)、classNamesは自動的に無視してくれます。グループごとに配列やオブジェクトを分けておくと、どの条件がどのクラス名に対応するのかをコードの見た目からも把握しやすくなります。
まとめ
classnamesは、条件付きのクラス名を組み立てるという一見地味な作業を、驚くほどシンプルに解決してくれるライブラリです。文字列・オブジェクト・配列・真偽値を自由に混在させて渡せる柔軟さと、falsyな値を自動で除外してくれる挙動のおかげで、テンプレートリテラルと三項演算子の組み合わせから解放されます。
依存ゼロで数百バイトという軽さも魅力なので、Reactに限らずクラス名を動的に組み立てる場面があれば、まず試してみる価値のあるライブラリです。
