2016年5月2日
Reactを使ったモジュラーCSS : CSS-in-JSとCSS Module
(2016-04-07)by Philippe Masset
本記事は 、原著者の許諾のもとに翻訳・掲載しております。
Buffer のメンバーはReactが大好きで、フロントエンドの多くのコードベースを徐々にReactに移行させています。ReactにFluxを加えると、モジュラー形式の小さなアプリでできた複雑なプロダクトを構築するための、とても健全な方法になると思います。そこで、1つ1つの新しい小さなアプリと機能を、大規模な構造体に追加される、Reactの新しいブロックと考えます。
私は最近、このような新機能の1つに取り組んでいますが、React+Fluxのアプリケーションを作るのがいかに簡単であるかと、その理由について、さらに夢中になってしまいました。Reactを使うと有意味なコンポーネントを集めてUIを宣言的に構築するのが楽になり、Fluxはその混成体に妥当なデータフローをもたらします。
複雑なアプリケーションを作るときに発生する課題について多くの考察がなされましたが、React+Fluxの組み合わせはその課題の多くを鮮やかに解決してくれます。
それでもまだ私は、Reactのモジュール性と再利用性を十分に生かしたスタイリングの作り方が分かりません。
幸いなことに、最近、CSSの世界ではとても興味深い発展がいくつかみられ、モジュラーCSSを実現するための多くのパターンとツールが出現しています。
CSS-in-JS
Vjeux は、 React: CSS-in-JS で、JSを活用してCSSの多くの課題を解決することについて素晴らしいアイデアを数多くシェアしています。性質上CSSで実現するのが困難な多くのことが著しく簡単になり、その理由は単にJSで実現する方が簡単だから、という原理です。
大まかに言えば、自然構文にとても近い感じがするJSのオブジェクトリテラルの形でCSSを記述し、 style 属性を使ってReactコンポーネントに適用するということです。スタイルを個別のモジュラーファイルに保管しておいて、そのファイルを通常の import 文を使ってJSモジュールにインポートすることができます。
CSSモジュール
CSSモジュールは、全てのクラス名とアニメーション名がデフォルトでローカルにスコープされたCSSファイルです。
これは、 CSSモジュール の非常にシンプルな前提です。ReactコンポーネントはそれぞれのCSSファイルを取得し、そのCSSファイルはそのファイルとそのコンポーネントにスコープされています。構築時に、魔法が起こります。非常にシンプルで衝突の危険がないローカルクラス名が、自動生成された名前にマッピングされ、React内で使うためのJSオブジェクトリテラルとしてエクスポートされます。
CSSモジュールは意図的にとてもシンプルなものになっています。CSSのモジュラー性の課題を解決するために、ごく少ない新しい構文をCSSに追加しただけで、その他の部分は変わらず同じです。
モジュラー性の課題を解決する
課題の多くは、CSSルールがグローバルスコープの中にあることに起因します。CSS-in-JSとCSSモジュールがそれらの課題をどのようにして解決するか説明しましょう。
ネームスペースと、特定性の衝突の回避
CSS-in-JSとCSSモジュールはデフォルトでローカルにスコープされます。これは、ネームスペースが不要であることを意味します。また、BEMのような命名規則では対応できない、とても短くて意味のある名前をスタイルルールに使えるようにもなります。その結果、命名の衝突や特定性の問題が起こる危険がなくなります。
スタイルを定義するには、CSS-in-JSではオブジェクト属性 { image: {} } を使用します。
CSSモジュールはシンプルなクラス名 .image {} を使用します。
不要なコードの削除
複雑なCSSセレクタでは、あるスタイルが使われるのか、または、どこで使われるのかを知るのが困難なことがよくあります。モジュールと明示的な従属性を使用すると、どのスタイルが使われるのか使われないのかを、ずっと簡単に知ることができ、使用されないスタイルを確信を持って削除することができます。また、CSS-in-JSとCSSモジュールの両方について、ここではローカル変数の話をしているので、ビルドツールを使っても、使用されないローカル変数を見つけて取り除くことができます。
コンポジション
どちらの手法にも、スタイリングの再利用の解決策としてコンポジションが使えます。
その目的のために、CSS-in-JSでは、 Object.assign() など、JSが提供するはずのものは何でも利用することができます。
CSSモジュールでは、 composes ルールを使ってセレクタを構成することができます。
従属性
CSS-in-JSは簡素なJSなので、どんなモジュールシステムでもスタイルをエクスポートまたはインポートすることができます。
CSSモジュールでは、他のCSSモジュールからのクラス名をコンポーズすることが可能になります。
条件付きスタイリング
状態と、その他の条件に基づいてスタイルを変更することもまた、コンポジションの良い使い方です。
CSS-in-JSの場合:
CSSモジュールの場合:
呼び出し元によるカスタマイズ
あるコンポーネントのスタイルは、そのコンポーネントの呼び出し元によってカスタマイズされるべきです。再利用可能なコンポーネントに、例えばディメンションやデフォルト色がそのコンポーネント自体にスコープされるようなコンポーネント固有のスタイルを持たせ、後に親が各自でスタイルを操作して位置決めしたり、可能なら、見た目や雰囲気を微調整するようにしたりすれば自由度が高くなると思います。
CSS-in-JSでは、親から子へのスタイルの受け渡しは、スタイルオブジェクトをマージするだけなので簡単です。
CSSモジュールでは、クラス名を連結させることによって同じことができます。
JSを使うとCSSより少し簡単にできるクールな小技
CSS-in-JSとCSSモジュールのどちらでも、スタイリングをモジュール化する優れた解決策が得られ、それは実に素晴らしいものです。モジュール性に加え、他にもスタイリングにとって望ましい多種多様な技が得られる可能性があります。そして、JavaScriptはその性質から、それらの技をvanilla CSSよりずっと簡単に成し遂げることができるのです。
でも、こういったおまけの技は、CSSでも、LESSやSASSなどのCSSプロプロセッサに頼るか、将来のCSS機能を利用するために前もって新しい構文を使う(そして、この構文をプリプロセスして、今のブラウザで実行するようにする)かのどちらかによって達成することができます。
このような、今はJSを使った方が簡単に達成できると思われることに、CSS-in-JSとCSSモジュールがどのように拮抗するのか説明しましょう。私たちはCSSモジュールでは、LESSやSASSではなく、近日追加されるCSSの機能を使いますが、LESSやSASSも選択肢として考えられます。
スタイル定数の共有
スタイル定数は、色やサイズなどの共通のプロパティを、コンポーネント間または同一コンポーネント内で共有するための優れたツールです。
CSS-in-JSの場合は全てがJSなので、スタイル定数の共有は、変数を使用すること、そして、他のコンポーネントが利用できるようにエクスポートすることと同じぐらい簡単です。
CSSモジュールの場合も、同一のスタイルシート内で定数を共有することは、 CSS Custom Properties を使えば簡単です。
しかし、このような定数をスタイルシート の間 で共有することは、あまり便利ではありません。その理由は、CSSモジュールは、スタイルをまとめてコンポーズするように(そして、他のファイルからスタイルをインポートすることによって非常に簡単にコンポーズできるように)意図されており、定数の共有よりもスタイルコンポジションの使用が期待されるからです。
したがって、CSSモジュールで意図される方法は、次のようになります。
でも、どうしても定数を共有したいのなら、CSSモジュールの土台となる ICSS を使う必要があります。これは標準のCSSに加えられた小さな仕様で、CSSモジュールを有効にするための疑似セレクタ、 :import と :export が追加されています。これらを使用すると、キーと値のペアのエクスポートとインポートが可能になり、このペアをローカルでCSSのカスタムプロパティにマッピングすることができます。
くどいコードですが、各ブロックには妥当な目的があります。最初のブロックは外部従属性から値を明示的にインポートし、第2のブロックはこのインポートされた値を受け取るローカルのカスタムプロパティを定義し、次に適切に見えるようにカスタムプロパティを使うことができます。
グローバルを導入するのは危険で、私たちはその危険を積極的に除外しようと試みていますが、定数の共有には賛否両論があります。危険を冒してでも短いコードを書きたい人にも、残された道はあります。グローバルのカスタムプロパティを使うと、上のコードを次のように短くすることができます。
JSとCSS間で変数を共有する
JSの変数をCSSと共有することは理想的だとは思えませんが、多くの状況で共有が必要となる場合があります。
CSS-in-JSでは、JavaScriptがこの共有を再度非常に簡単にしてくれます!
CSSモジュールの場合はどうかと言うと、残念ながら現時点では少し制約があります。
CSSの attr() 関数 は、所与のDOMの属性の値を取得し、それをCSSで使用することができますが、ブラウザがサポートするのは、この関数が疑似要素のcontentプロパティで使われ、しかもストリング値と一緒の場合のみです。ゆくゆくは、 calc() のような、他のCSS関数を併用して、あらゆるDOMの属性は、CSSが読み取れ、計算することができる値を保持することができるでしょう。JS + HTMLとCSS間を強力に結びつけることができるというわけです!
理想:
将来のバージョンのChromeが CSS3 で attr() の定義をサポートすると仮定した場合、この理想は以下のようになるでしょう。
一方、JSとCSS間で変数を共有するために疑似要素と文字列よりも多くのものを必要とする使用事例において、取るべき方法はインラインスタイルです。
スタイルの計算と操作
CSSモジュールを使っているならば、近日追加されるCSSの仕様が、プロパティを操作するための calc() や color() のような便利な関数を多く導入してくれます。
そしてJavaScriptの表現力が、思い付く限りの方法でスタイルを操作するために、理想的な候補となります。これには、 色 も含まれます。
JSでは困難な一般的なCSSの機能
JSでは、スタイルの追跡をCSSよりも容易にしてくれます。
しかし、ドキュメントがどのように表示されるかを記述するために作られた言語であるCSSは、様々な方法でこれらのドキュメントのスタイリングが簡単になるように進化してきたということを、忘れないでください。JSは、同じことを目的としていません。それに、一部のスタイリングの使用事例に関して言えば、CSSにある簡潔性がいくらか欠如しています。CSSが疑似クラス、疑似要素、そしてメディアクエリですることを、JSで実現することは難しいのです。
疑似クラス
CSSでは、 :first-child や :nth-child といった疑似クラスは、ドキュメント内の他の要素との関係に基づいて、スタイリングの要素を可能にします。
JSを使ったスタイリングの場合、これらの疑似クラスは関係ありませんが、レンダリングのループ内で、いくつかの要素に対してスタイリングを選択的に適用することは可能です。例えば、疑似クラス :nth-child(even) に等価なコードは、JSでは以下のようになります。
JSでの要素のスタイリングが、ループ内のインデックスといった単純条件に基づく必要がある場合、そのメソッドはCSSよりも若干くどくはなるものの、それでも簡単に扱うことができます。JSで、これらの疑似クラスに相当する、クールなカンニングペーパーを紹介しましょう( インラインスタイルに関する、素晴らしいプレゼンテーションから抜粋しています )。
疑似クラスはまた、ページとのインタラクションに基づいてスタイルを変更するのに有効です。これは、CSSお馴染みの :hover や :focusand、:active が真に輝ける場所です。
JSでは、 onMouseEnter や onMouseLeave といったイベントリスナを、コンポーネントの状態をアップデートするために使われなければならざるを得ず、状況に応じて”hover”スタイルを適用しなくてはなりません。
これはかなり扱いにくく(間違いなくより強力ではあるのですが)、多くのライブラリがこの使用を取りやめています。これらの中で、私が特に気に入っているのが、 Radium で、hoverスタイルは以下のようになります。
疑似要素
CSSでは、ドキュメントの特定部分をスタイルする際、 ::after や ::before、::placeholder をしばしば重宝します。また、 ::first-letter や ::first-line 、 ::selection にも同様のことが言えます。
JSに関して言えば、これらを簡単に真似ることは困難です。確かに、 ::after や ::before の代わりに実際の要素を使うこともできるでしょうが、 ::first-letter または ::first-line を対象とすることが厄介になるでしょう。それに、スタイリング目的のために要素を模倣する抽象化をしなければ、 ::placeholder または ::selection を対象とすることができませんが、この抽象化をすることは少しやり過ぎです。
メディアクエリ
JSでメディアクエリを扱うためには、条件付きでスタイルを適用するために window.matchMedia() を当てにすることもまた、同等の機能を持つ快適なCSSを使うよりも、あまり現実的ではありません。Radiumのようなライブラリを役立てることもできます。
始めてみよう
CSS-in-JSを使う
結局のところ、CSS-in-JSは、インラインスタイルでしかないので、Reactの style 属性を活用することで使い始めることができます。
通常、vanilla CSSに備わってくる、 :hover や :focus 、 active といった疑似クラス、メディアクエリといったいくつかの機能において、Radiumのようなライブラリを使うことは有用です。また、Radiumは自動でプリフィックスも付与してくれます。JSでスタイリングするということはつまり、Autoprefixerといった通常のCSSツールの使用をやめるという最高の考え方です!
CSSモジュールを使う
スタイリングしたいReactのコンポーネンにおいては、単純に、そのコンポーネントのためのスタイルを含むCSSファイルを作成します。ファイルにある全てのスタイルはローカルであり、そのコンポーネントのコンテキスト内でしか使えないはずなので、シンプルなクラス名だけを使用すればいいのです(例えば .image など)。
ファイルを作成したら、ReactのコンポーネントにCSSファイルをインポートします。他のものをインポートするのと同じ要領です。そして、 className の属性を使って、インポートされたクラス名を適用します。
アプリを構築するには、Webpackにある css-loader や modules のオプション を使うことができます。出力は以下のようになります。
Webpackコンフィグの例 では、JSのバンドルと一緒にCSSのバンドルを作成するために、 ExtractTextPlugin が使われています。また localIdentNName のcss-loaderオプションは、開発の際にスタイルを位置付けるのを手助けするために、適当なクラス名を作成するのに役立ちます(短いハッシュは、プロダクションコードに使用することができます)。
CSSモジュールは、以下にも使用することができます。
-
Browserifyで、 css-modulesify プラグインを使う場合(これはまだ実験段階です。私は過去にこれを使用して、問題に出くわしたことがあります)。
-
統計サイトや、サーバサイド、PostCSSプラグイン postcss-modules 。
-
Reactを使用しない場合!
どちらを選ぶ?
CSS-in-JS、CSSモジュールどちらのアプローチも、モジュール性やコンポーザビリティ、ローカルスコープの解放的な体験などの点で、同等の利点をもたらしているように思えます。しかし、利点は同等であっても、実装は異なります。一方は、CSSファイル内に存在する実際のCSSであり、インポートやハッシュ化されたクラス名を使っ構築時にJSモジュールと結合されます。もう一方は、最初から実際のJSであり(CSSに酷似していますが)、実行時に構成され、インスタイルスタイルの形で要素にアタッチされます。。どちらも対処する内容によって、一方よりも簡単な操作ができるようになっていますが、両方とも全てを行うことは可能です!
確かなことは、どちらを選ぶかは使用事例や個々の好み次第ということです! CSSモジュールには他の構築ステップが加わりますが、 Pete Hunt はCSS-in-JSについて、一部のツールやパフォーマンスの遅延をほのめかしながら、次のように雄弁に語っています。「”インラインスタイルのあらゆることに対して”いまだ、ブラウザベンダーによるにじみ出るような努力が見られない」。
今回私は、CSSモジュールを使用することにしました。CSSの方が、私の仕事に適したツールだと実感したからで、CSSモジュールと近日追加されるCSSの強力な機能である前処理 ^(1) , ^(2) の組み合わせに大変満足しています。保全性や明確さ、信頼性、そしてコードにあるべき美しさにおいて、これは有用です。
しかし、CSS-in-JSは代替えとして使える素晴らしいものだと思います。CSS-in-JSを使用している方の体験談、そしてこの CSS-in-JS戦略の次のレベル に関する、皆さんの声を是非聞かせてください。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa