9つのおとぎ話
CSSは迷走しています。JavaScriptでドキュメントをスタイリングしているプロジェクトでは、多くの場合誤った理由からその方式を選択しています。本稿では、よくある誤解(神話)を列挙し、そうした問題に対するCSSソリューションを紹介します。
本稿は、特定のプロジェクトや人物への攻撃を意図するものではありません。ここでは、“CSS in JavaScript”(CSS in JS)をstyled-components
を使用することと定義します。これは、Reactのコンポーネントをスタイリングする最近のトレンドとなっています。
styled-components の作者であるMax StoiberとGlen Maddern、また彼らに協力した人々は皆、卓越したアイデアと善意にあふれる優秀な人々です。
完全な透明性のために断っておくと、私はreact-css-modulesとbabel-plugin-react-css-modulesの作者でもあります。
赤ずきんちゃん
CSSとJavaScriptの歴史
Cascading Style Sheets(CSS)は、マークアップ言語で書かれたドキュメントの表示方法を記述するために作成された言語です。JavaScriptは、画像やプラグインなどのコンポーネントを組み立てるための“グルー言語”(グルーは「糊」の意味)として作成されたものです。長い年月の間にJavaScriptは成長し、変更が加えられて、新しいユースケースに採用されてきました。
Ajaxの出現(2005年)は画期的な出来事でした。この時Prototype、jQuery、MooToolsなどのライブラリが多数のコミュニティに評価されたことでコラボレーションの機運が高まり、バックグラウンドでのデータ取得やブラウザ間の不一致といった問題の解決につながりました。その一方で、全てのデータをどう管理するかという、新たな問題も発生しました。
話を2010年まで早送りすると、アプリケーションの状態管理の業界標準として、Backbone.jsが使われるようになりました。程なくして、KnockoutとAngularが双方向のバインディングで皆を魅了します。さらにその直後、ReactとFluxが相次いで登場します。これにより、コンポーネントで構成されるアプリケーション、即ちシングルページアプリケーション(SPA)の時代が幕を開けました。
CSSについてはどうなのか?
styled-components
のドキュメントには、以下の記述があります。
プレーンなCSSの問題は、Webがドキュメントで構成されていた時代に構築されたことだ。Webは1993年に、主に科学論文などをやり取りするために構築されたもので、CSSはそうしたドキュメントをスタイリングするためのソリューションとして導入された。しかし今日では、ユーザーと双方向的なやり取りを行うための、豊富な機能を備えたアプリケーションを構築している。CSSはこうしたユースケースには向いていない。
私はこれに同意できません。
CSSは進化を遂げ、モダンなUIの要件を取り込みました。過去10年間に追加された機能の数は非常に多いです(擬似クラス、擬似要素、CSS変数、メディアに対するクエリ、キーフレーム、コンビネータ、列、フレックスボックス、グリッド、計算値など)。
UIの観点からいえば、“コンポーネント”はドキュメントを構成する独立した要素です(例えば<button />
はコンポーネントです)。CSSは、ドキュメントをスタイリングする目的で設計されているものです。従って、全てのコンポーネントがスタイリングの対象となります。では、何が問題なのでしょう?
ことわざにもあるとおり、“適材適所”ということです。
styled-components
styled-components
を使うと、 CSS in JSでタグ付きのテンプレートリテラルが使用できるようになります。この場合、コンポーネントとスタイルとの間のマッピングは解除され、コンポーネントは低レベルのスタイリング定義となります。その例を以下に示します。
import React from 'react'; import styled from 'styled-components'; // Create a <Title> react component that renders an < h1> which is // centered, palevioletred and sized at 1.5em const Title = styled.h1` font-size:1.5em; text-align: center; color: palevioletred; `; // Create a <Wrapper> react component that renders a < section> with // some padding and a papayawhip background const Wrapper = styled.section` padding:4em; background: papayawhip; `; // Use them like any other React component – except they're styled! <Wrapper> <Title>Hello World, this is my first styled component!</Title> </Wrapper>
結果は次のとおりです。
styled-components
は現在、 Reactでコンポーネントをスタイリングする新しい手段としてもてはやされています。
しかしここで1つ、はっきりさせておきたいことがあります。styled-components
は、CSSよりハイレベルの部分を抽象化したものに過ぎません。JavaScriptで定義されたCSSを解析し、CSSにマッピングされたJSX要素を作成するだけです。
私はこの傾向を好みません。多くの誤解が含まれているからです。
私はIRC、Reddit、Discordでstyled-componentsを使っている人々を対象として、その理由を調査し、その選択をする人々に共通する回答のリストをまとめました。これを私は「神話」と呼んでいます。
神話1: グローバルの名前空間とスタイルの衝突を解決してくれる
私がこの回答を神話と呼んでいるのは、こうした問題がこれまで解決されていなかったかのように聞こえるからです。コミュニティでは大昔から、CSSモジュール、Shadow DOM、数え切れないほど多くの命名規則(例:BEMによって、この問題を解決してきました。
styled-components
は(CSSモジュールと同様に)人間が命名することの責任を軽減するものです。人間は間違いを犯すものですが、コンピュータのミスは、人間ほど頻繁ではありません。
しかしそれだけでは、styled-components
を使う十分な理由にはなりません。
神話2: styled-componentsを使えばコードがより簡潔になる
このことは、以下の例を使って説明されることが少なくありません。
<TicketName></TicketName> <div className={styles.ticketName}></div>
まず指摘したいのが、これはあまり関係ありません。使おうが使うまいが、大差ないのです。
2点目の指摘として、この神話には一部に誤りが含まれています。文字数の総計は、スタイル名によって変動するからです。
<TinyBitLongerStyleName></TinyBitLongerStyleName> <div className={styles.longerStyleName}></div>
本稿で後述する(神話5:コンポーネントを簡単に条件付きでスタイリングできる)とおり、スタイルの定義についてもこれは当てはまります。styled-components
を使えば記述が簡潔になるのは、最も基本的なコンポーネントだけで構成されている場合に限定されるのです。
神話3:styled-componentsを使えばセマンティクスについてよく考えるようになる
そもそも前提が間違っています。スタイリングとセマンティクスが表すのは別々の問題であって、別々のソリューションが必要です。Adam Morse(mrmrs)の言葉を借りれば、次のとおりです。
コンテンツのセマンティクスは、視覚的スタイルには全く関係ない。レゴで何かを作るとき、「おお、これはエンジンブロックのピースだ」などとは決して思わず、「やった、1×4の青レゴだ。これで何でも好きなことができるぞ」と思ったものだ。作っているのが海底探検基地だろうが飛行機だろうが、そのレゴブロックの使い方は正確に知っていた。
– http://mrmrs.io/writing/2016/03/24/scalable-css/
(Scalable CSSに関するAdamの記事を一読されるよう強くお勧めします。)
とはいっても、とにかく始めましょう。あるリンクがあると仮定します。
例:
<PersonList> <PersonListItem> <PersonFirstName>Foo</PersonFirstName> <PersonLastName>Bar</PersonLastName> </PersonListItem> </PersonList>
セマンティクスでは、マークアップを適切なタグを使って構築しているのかが問われます。このコンポーネントがどんなHTMLタグをレンダリングするか分かりますか? 分かりませんね。
これと比較してみましょう:
<ol> <li> <span className={styles.firstName}>Foo</span> <span className={styles.lastName}>Bar</span> </li> </ol>
神話4:スタイルを簡単に拡張できる
v1では、styled(StyledComponent) constを使ってスタイルを拡張することができます。v2では、既存のスタイルを拡張するためにextend
メソッドが導入されました。例えば、次のとおりです。
const Button = styled.button` padding: 10px; `; const TomatoButton = Button.extend` color: #f00; `;
素晴らしい。しかし、同じことはCSSでもできます(または、CSSモジュール合成や、SASS継承mixin@extendを使って)。
button { padding: 10px; } button.tomato-button { color: #f00; }
JavaScriptの方が簡単だと言えるでしょうか?
神話5:コンポーネントを簡単に条件付きでスタイリングできる
例えば、次のように、コンポーネントを属性に応じてスタイリングできるという考えです。
<Button primary /> <Button secondary /> <Button primary active={true} />
これは、Reactの世界では大いに理にかなっています。結局、コンポーネントの振る舞いは属性を使ってコントロールされます。スタイルに属性値を直接アタッチすることは合理的なのでしょうか? そうかも知れませんが、コンポーネントの実装を調べてみましょう。
styled.Button` background: ${props => props.primary ? '#f00' : props.secondary ? '#0f0' : '#00f'}; color: ${props => props.primary ? '#fff' : props.secondary ? '#fff' : '#000'}; opacity: ${props => props.active ? 1 : 0}; `;
JavaScriptの力を利用して条件付きでスタイルシートを作るのは、とても便利です。しかし、それは、スタイルの解釈が難しくなるということでもあります。CSSと比べてみましょう。
button { background: #00f; opacity: 0; color: #000; } button.primary, button.seconary { color: #fff; } button.primary { background: #f00; } button.secondary { background: #0f0; } button.active { opacity: 1; }
この場合、CSSの方が短く(229文字と222文字)、流れを追いやすいです(サブジェクト指向)。さらにCSSでは次のように、プリプロセッサを使って、もっと短くし、グループ分けすることもできます。
button { background: #00f; opacity: 0; color: #000; &.primary, &.seconary { color: #fff; } &.primary { background: #f00; } &.secondary { background: #0f0; } &.active { opacity: 1; } }
神話6:コードの編成を改善できる
styled-components
が好きな理由として、スタイルとJavaScriptを同じファイルに入れられることを挙げる人もいました。
同じコンポーネントのために複数のファイルがあるのが煩わしいというのは理解できますが、スタイルとマークアップを1つのファイルに詰め込むソリューションは悲惨です。バージョン管理の記録追跡が難しくなるだけでなく、どのコンポーネントも、簡単なボタンではなくロングスクロールになりがちです。
CSSとJavaScriptを同じファイルに入れる必要があるのなら、css-literal-loaderの使用を考えてみましょう。これを使えば、ビルド時にextract-text-webpack-pluginを使ってCSSを展開し、そのCSSを自分の標準ローダ構成を使用して処理することができます。
神話7:DXが改善されるし、ツール類が素晴らしい
これを理由に挙げる人は、styled-components
を使ったことがないのが明白です。
- スタイリングに何か不具合が生じると、長大なスタックトレースエラーと共にアプリケーション全体がクラッシュします(v2使用時の個人的体験です)。それに比べて、CSSでは”スタイルエラー”があっても、単に要素が正しくレンダリングされないだけです。
- 要素に識別可能な
className
がないので、デバッグ時にReactの要素ツリーとDevToolsのDOMツリーの間で振り回されることになります(レコードについては、v2では、babel-plugin-styled-components
を使って対処できます)。 - リンティングがありません(開発中のstylelintプラグインはあります)。
- 無効なスタイルは単純に無視されます(例えば、
clear: both; float left; color: #f00;
ではエラーや警告は起こらず、styled-components
のソースコードを読んでいるだけなのに、デバッグ中にスタックトレースを調べるのに15分間もかかりました。そして、あるチャットにコードをコピー&ペーストして質問し、ある人に指摘されてはじめて:
が抜けていることに気付きました。あなたは気付いていましたか?)。 - シンタックスハイライト、コード補完などといったIDEの細かな長所をサポートしているIDEはほとんどありません。財務や行政の当局の仕事をしているのなら、Atom IDEはおそらく検討の対象外でしょう。
神話8:パフォーマンスの改善とバンドルサイズの縮小ができそう
styled-components
は、そのままでは(https://github.com/webpack-contrib/extract-text-webpack-pluginを使用するなどして)静的CSSファイルに拡張することはできません。つまり、ブラウザはstyled-components
がスタイルを解析してDOMに追加してからでないと、スタイルの解釈を開始できないのです。- ファイルが個別になっていないので、CSSとJavaScriptを別々にキャッシュすることはできません。
- スタイル設定されたコンポーネントは全て、その性質上、追加HoCにラッピングされます。これは不必要なパフォーマンスヒットです。私がhttps://github.com/gajus/react-css-modulesを放棄した(そして、https://github.com/gajus/babel-plugin-react-css-modulesを作成した)理由は、これと同じアーキテクチャ上の欠陥です。
- HoCに起因して、サーバサイドをレンダリングする場合には、マークアップドキュメントが巨大なものになってしまいます。
- キーフレームではなく動的なスタイル値を使ってコンポーネントのアニメーション化を始めるのは嫌です。
神話9:応答型コンポーネントを開発できる
これは、主として、周囲の環境、例えばペアレントコンテナのディメンション、子の数などに基づいてコンポーネントをスタイリングする能力に関する話です。
まず、styled-components
は、これに何の関係もありません。このプロジェクトの範囲外です。この場合は、コンポーネントのstyle
値を直接設定して余分なオーバーヘッドを回避した方が賢明です。
ただし、エレメントクエリは興味深い問題で、主にプロジェクトEQCSSと、それに相当するプロジェクトにとっては、CSSで注目される話題になりつつあります。エレメントクエリはシンタックスでは@media
クエリに似ていますが、エレメントクエリが特定の要素で動作するという点で異なります。
@element {selector} and {condition} [ and {condition} ]* { {css} }
{selector}
は、1つまたは多数の要素を目標とするCSSセレクタです。例:#id
、.class
など{condition}
は、測定基準と値から成ります。{css}
は、任意の有効なCSS規則を含むことができます(例:#id div { color: red }
)。
エレメントクエリにより、min-width
、max-width
、min-height
、max-height
、min-characters
、max-characters
、min-children
、max-children
、min-lines
、max-lines
、min-scroll-x
、max-scoll-x
などの条件を使って要素をスタイリングすることができます(http://elementqueries.com/を参照してください)。
ニャオーン!
いつの日かCSSの仕様にEQCSSの変種が登場することを祈りましょう。
ちょっと待って!
上に挙げた問題のほとんどは(全部ではなくても)、長期的には、コミュニティ、Reactまたはstyled-components
そのものの変更のいずれかによって解決できるでしょう。でも、大事なことは何でしょうか? CSSはすでに幅広く支持され、巨大なコミュニティに見守られてうまく機能しています。
この記事のポイントは、読者にJavaScriptで”CSS”を使わせないようにすることや、styled-components
を使わせないようにすることではありません。styled-components
を使ったスタイリングには、クロスプラットフォームのサポートという優れた適用事例があります。でも、間違った理由では使わないようにしましょう。
では、今は何を使えばいいのか
Shadow DOM v1を使うのは時期尚早です(51%グローバルサポート)。どちらかの命名規則でCSSを使いましょう(BEMをお勧めします)。クラス名衝突が心配なら(または、BEMを使うのが面倒なら)、CSSモジュールを使いましょう。React web向けの開発なら、babel-plugin-react-css-modulesの使用を検討してみてください。React native向けの開発なら、styled-componentsが好適です。