この記事は KIT Developer Advent Calendar 2017 の 18 日目の記事です。
はじめに
React で作成するコンポーネントにスタイルを適用する方法は大きく分けて以下の 4 つがありますが、どの方法にもメリットやデメリットがあり、一概にどれが優れているとは言えないのが正直なところです。本記事ではそれぞれの手法を紹介し、比較します。
- クラス名によるスタイリング
- インラインスタイル
- CSS Modules
- CSS in JS
扱う構文
本記事では以下の仕様・構文を扱います。
- ES2015, ES2016, JSX
- オブジェクトを受け付けるスプレッド演算子 (Stage 3, Proposal)
-
クラスの public フィールド (Stage 2, Proposal)
- 現在は private フィールドの提案とマージされて class-fields(Stage 3) になっています。
コンポーネントのスタイリング
1. クラス名によるスタイリング
CSS や SASS、LESS を使う場合、Webpack などのモジュールバンドラや gulp などのタスクランナにより CSS として出力し、HTML からスタイルを読み込み、クラス名に基づき、スタイルを当てます。この方法では既存のコードやビルドプロセスをそのまま使うことができます。そのため、スタイリングに関しては新しいことを学習する必要がありません。ただ、JSX でクラス名を指定する場合は class 属性ではなく className 属性を用いる必要があります(class は予約語となっているため)。
.button {
min-width: 64px;
line-height: 32px;
border-radius: 4px;
border: none;
padding: 0 16px;
color: #fff;
background: #639;
}
import React from "react";
export default (props) => <button className="button" {...props} />;
import React from "react";
import ReactDOM from "react-dom";
import Button from "./button.js";
ReactDOM.render(<Button>Button</Button>, document.querySelector("#app"));
後は button.css とトランスパイルした app.js を HTML から読み込み、button.js を DOM にマウントすると、シンプルなボタンが出来上がります。
クラス名によるスタイリングを用いるメリットは CSS や SASS、LESS が使えることですが、同時にデメリットでもあると言えます。上記の例だけではあまり問題を感じませんが、クラス名の数が増えてくると、"CSS において、クラス名の指定は常にグローバル"ということから以下のような問題に直面します。
- どこで上書きするか、どこから上書きされるかを予測することが困難である
- 上書きを避けるために命名に工夫をする必要がある
例えば Bootstrap は 600 以上の数のクラス名を使います。Bootstrap を使っている時や複数人で開発している時に自分が指定したクラス名が使われていることにより、開発中の画面の見た目が予期せぬ結果になる可能性や、他の画面の見た目が変わる可能性が考えられます。開発中の画面の場合は気がつくことが出来るかもしれませんが、他の画面の変更に気がつくことは難しいでしょう。
CSS ではこのような問題を命名にいくつかの規則を設けることで解決してきました。有名な命名規則として OOCSS、BEM、SMACSS などがあります。
- CSS / Methodology / BEM
- bem-methodology-ja/index.md at master · juno/bem-methodology-ja
- CSS の設計方法をまとめてみた~BEM 編~ - Qiita
クラス名は命名規則によって慎重に指定されなくてはいけないということは CSS の設計全般に言えることでしたが、次に紹介する問題は主にコンポーネント指向で多く発生します。問題を紹介する前にサンプルのコンポーネントを示します。
import React from "react";
export default props => {
const { primary, ...other } = this.props;
const className = ["button", primary && "button--kind-primary"].filter(Boolean).join(" ");
return <button className={className} {...other} />;
};
Button コンポーネントは primary 属性 を受け取らなければ(*1) Button を表示し、primary 属性を受け取った場合(*2)、Primary Button を表示するコンポーネントです。コンポーネントは構造・見た目・振る舞いを 1 つにまとめたもので、Prop により、それらの要素を自在に扱うことが出来ました。primary 属性により、Button の見た目を変えるという使い方はコンポーネントにおいてよくある使い方であるといえます。
クラス名ではなく、カスタムデータ属性(data-*
)を活用することも出来ます。この場合、 .button--kind-primary
ではなく .button[data-is-primary="true"]
でスタイリングします。
import React, { Component } from "react";
export default props => {
const { primary, ...other } = props;
return <button className="button" data-is-primary={primary} {...other} />;
};
この例では Prop により、予め用意したスタイルを切り替えることが出来ました。では、次のような場合はどうでしょうか。
- Button の背景色を props.background で指定したい
- Avatar など、縦横比が 1 のコンポーネント (width, height を props.size で指定したい)
このように、Prop の値をスタイルにひも付ける操作をクラス名や属性値などのセレクタの指定で行うことは不可能です。これの問題を解決するには後述のインラインスタイルや CSS in JS を使う必要があります。
*1: 厳密には primary の評価結果が false であれば
*2: 厳密には primary の評価結果が true であれば
2. インラインスタイル
React における JSX の style 属性は文字列では無くオブジェクトを受け取ります。例えば、1 で最初に示した button.js
は次のようになります。しかし、一般的に style 属性を用いてスタイリングすることは公式ドキュメントの DOM Elements - React において推奨されていません。style 属性はドキュメントなどで例を示す場合や動的で頻繁に計算されるスタイルを追加する場合のみに使われるべきです。
import React from "react";
const style = {
minWidth: 64, // 数値は"64px"のように、pxとして扱われます
lineHeight: "32px",
borderRadius: 4,
border: "none",
padding: "0 16px",
color: "#fff",
background: "#639"
};
export default (props) => <button style={style} {...props} />;
また、インライン CSS ではなく、インラインスタイルであるように、style にはスタイルしか渡すことが出来ません。つまり、 :hover
や :focus
などの擬似要素セレクタやメディアクエリを使うことは出来ません。state を onMouseEnter や onMouseLeave イベントの発火時に操作し、state の値によりクラス名に hover 加えたり、外したりすることで"hover 時のスタイル"を当てることは出来ますが、CSS でシンプルに実現できる操作を却って複雑に実装することはあまり良い選択だとは言えないでしょう。
次に、動的なスタイルの適用について紹介します。CSS で実現できなかった部分ですね。
- Button の背景色を props.background で指定したい
import React from "react";
const styleGenerator = ({ background }) => ({
minWidth: 64,
lineHeight: "32px",
borderRadius: 4,
border: "none",
padding: "0 16px",
color: "#fff",
background: background ? background : "#639"
});
export default (props) => <button style={styleGenerator(props)} {...props} />;
- Avatar など、縦横比が 1 のコンポーネント (width, height を props.size で指定したい)
import React from "react";
const styleGenerator = ({ size, image }) => ({
minWidth: size,
maxWidth: size,
minHeight: size,
maxHeight: size,
borderRadius: "50%",
backgroundImage: `url(${image})`,
backgroundSize: "contain"
});
const Avatar = props => <div style={styleGenerator(props)} {...props} />;
Avatar.defaultProps = {
size: "64px"
};
export default Avatar;
import React from "react";
import ReactDOM from "react-dom";
import Button from "./button.js";
ReactDOM.render(
<Avatar
size="48px"
image="https://ja.gravatar.com/userimage/34020782/a317979a82b17d299c5c8decb48523c6.png"
/>,
document.querySelector("#app")
);
実行結果は以下の通りです。
props を受け取ってスタイルのオブジェクトを返す関数を定義して呼び出しているだけですね。
3. CSS Modules
さて、ここまでで BEM などを使ったクラス名の指定とインラインスタイルによる動的なスタイリングを紹介しました。BEM はグローバルな名前空間に規則と秩序をもたらしましたが、規則により名前が複雑になってしまいます。命名規則によって作られた長いクラス名が好ましくない場合、CSS Modules を用いて CSS にローカルスコープを導入することで解決することが出来ます。
React と Webpack、Babel の環境では css-loader の CSS Modules、react-css-modules または babel-plugin-react-css-modules のいずれかを使用することで CSS をモジュール化することができます。
babel-plugin-react-css-modules を使う方法は他の方法よりもコードを簡潔にすることが出来るため、この記事では babel-plugin-react-css-modules のみを紹介します。
また、css-loader で CSS Modules をやることについて気になる場合は下記の記事がおすすめです。
babel-plugin-react-css-modules において、ローカルスコープを持つスタイルをひも付けるには className 属性ではなく、styleName 属性を利用します。button.css
内のクラスセレクタは css-modules により、ファイルパスとランダムな文字列から推測困難でユニークな識別子に変更されます。styleName 属性は変更前のクラス名を指定すると、babel-react-css-modules により変更後のクラス名を指定した className 属性にトランスパイルされます。
.button {
min-width: 64px;
line-height: 32px;
border-radius: 4px;
border: none;
padding: 0 16px;
color: #fff;
background: #639;
}
.primary {
background: #3f51b5;
}
import React from "react";
import "./button.css";
export default (props) => <button styleName="button" {...props} />;
つまり、 button.js
は button.transpiled.js
のようなコードに変換されます。
(babel --plugins syntax-jsx,react-css-modules button.js
により再現することが出来ます)
import React from "react";
import "./button.css";
export default (props => <button className="src-___button__button___3f6Pb" {...props} />);
動的な styleName の指定も可能です。
import React from "react";
export default props => {
const { primary, ...other } = this.props;
const styleName = ["button", primary && "primary"].filter(Boolean).join(" ");
return <button styleName={styleName} {...other} />;
};
button.js
は 下記の button.transpiled.js
に変換されます。
(babel --plugins syntax-jsx,syntax-object-rest-spread,react-css-modules button.js
により再現することが出来ます)
var _this = this;
import _getClassName from "babel-plugin-react-css-modules/dist/browser/getClassName";
const _styleModuleImportMap = {
"./button.css": {
"button": "src-___button__button___3f6Pb",
"primary": "src-___button__primary___XCyBy"
}
};
import React from "react";
import "./button.css";
export default (props => {
const { primary, ...other } = _this.props;
const styleName = ["button", primary && "primary"].filter(Boolean).join(" ");
return <button {...other} className={_getClassName(styleName, _styleModuleImportMap)} />;
});
button.css
のクラス名と変換後のクラス名のマップがコードに書き出されていることがわかります。このマップを元に _getClassName
関数が styleName
を className
に対応付けているんですね。
4. CSS in JS
React のインラインスタイルが style 属性にスタイルを当てることのみが出来たのに対し、多くの CSS in JS のライブラリでは擬似要素セレクタやメディアクエリ、もしくは CSS の構文そのものを使用することが出来ます。
一言で CSS in JS といっても、それを実現するためのライブラリは多く、私はあまり把握できていません。
多くのライブラリはどのような実装になっているのかというと、
スタイルの再現に関する実装
<style>
タグを生成して、<head>
に insert する実装パターン
- 昨今のスタンダードなライブラリの使っている手法
ということで、動的に style タグを生成することで実現されているようです。
この記事では比較のため、 Radium と StyledComponents を紹介します。
Radium
Radium は React のインラインスタイルでは使えなかった、メディアクエリや一部のセレクタ(:hover
、:focus
、:active
)が使える他、スタイルのオブジェクトの配列によるスタイルの適用ができる CSS in JS ライブラリです。実装は Higher Order Components により、onMouseEnter や onMouseLeave などのイベント発火時に :hover
などのスタイルを追加・削除しているようです。
FormidableLabs/radiumにおいて、"概念的にインラインスタイルのシンプルな拡張"と説明されているように、style タグを生成する実装と違い、全ての CSS をサポートしていません。
import React, { Component } from "react";
import Radium from "radium";
import chroma from "chroma-js";
const styleCreator = ({ primary, secondary, background }) => {
const backgroundColor = primary ? "#3F51B5" : secondary ? "#E91E63" : background;
return {
minWidth: 64,
lineHeight: "32px",
borderRadius: 4,
border: "none",
outline: "none",
padding: "0 16px",
color: "#fff",
cursor: "pointer",
background: backgroundColor,
":hover": {
background: chroma(backgroundColor).darken()
}
};
};
class Button extends Component {
static defaultProps = {
background: "#009688"
};
render() {
const { primary, background, style, ...other } = this.props;
return <button style={[styleCreator(this.props), style]} {...other} />;
}
}
export default Radium(Button);
StyledComponents
StyledComponents はタグ付きテンプレートリテラルを活用することで JavaScript のオブジェクトではなく、CSS のシンタックスをそのまま使うことが出来るライブラリです。
"CSS in JS" をそのまま捉えると、オブジェクトではなく CSS が書ける StyledComponents は魅力的なのですが、JavaScript から言わせればただのテンプレートリテラルなため、エディターやコードビュアーが StyledComponents のシンタックスハイライトに対応していない場合は表示結果が残念になります。styled-components/styled-componentsによれば、主要なエディターは対応しているようですが、全てのコードビュアーが対応するのは難しそうです。ちなみに、VSCode では vscode-styled-components
をインストールすることで、以下のように表示できます。
また、これまで紹介してきた方法は className 属性や styleName 属性、style 属性にそれぞれクラス名やスタイル名、スタイルを与えており、JSX にスタイルに関する記述をする必要がありましたが、StyledComponents はテンプレートリテラル内でコンポーネントが受け取った Props を参照できるため、JSX をより簡潔に記述することが出来ます。
import React, { Component } from "react";
import styled from "styled-components";
import chroma from "chroma-js";
const backgroundColor = ({ primary, secondary, background }) =>
primary ? "#3F51B5" : secondary ? "#E91E63" : background;
const Root = styled.button`
min-width: 64px;
line-height: 32px;
border-radius: 4px;
border: none;
outline: none;
padding: 0 16px;
color: #fff;
cursor: pointer;
background: ${backgroundColor};
&:hover {
background: ${props => chroma(backgroundColor(props)).darken()};
}
`;
class Button extends Component {
static defaultProps = {
background: "#009688"
};
render() {
return <Root {...this.props} />;
}
}
export default styled(Button)``;
上記のコードは Radium で示した例と同等の表示結果になります。
まとめ
"コンポーネントのスタイリング"では、主に動的なスタイリングと CSS との互換性、スタイリングのスコープに焦点を当てて 4 つに分けたスタイリング手法を紹介しました。
動的なスタイリングはインラインスタイルや CSS in JS ライブラリを用いることで、実現することが出来ますが、インラインスタイルでは疑似要素セレクタやメディアクエリを使用することが出来ません。CSS in JS はこの問題を解決し、CSS と JavaScript によって高い表現力を持つことが出来ます。
また、スコープに関しては CSS Modules や StyledComponents を使用することで、重複の可能性が低いクラス名を生成し、命名規則の制約から逃れることが可能になりました。(インラインスタイルや Radium はクラスを使わないため、この問題には関係ありません。)
最後になりますが、紹介した手法を比較した表を作りましたので、こちらも参考にしていただければ幸いです。
CSS | SASS, LESS | Inline Style | CSS Modules | Radium | StyledComponents | |
---|---|---|---|---|---|---|
CSS like syntax | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
Supports CSS | ✅ | ✅ | ❌ | ✅ | 😕 *2 |
✅ |
Scope *1 | 🌏 | 🌏 | 🗾 | 🌏 or 🗾 | 🌏 or 🗾 | 🌏 or 🗾 |
Dynamic Styling | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ |
Syntax Highlighting | ✅ | ✅ | 😕 | ✅ | 😕 | 😕 |
Auto Prefixing *3 | 😕 *4 |
😕 *4 |
✅ | ✅ | ✅ | ✅ |
Pre-build | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
*1: 🌏 と 🗾 はそれぞれ Global と Local です
*2: 部分的なサポート
*3: スタイルのプロパティにベンダープレフィックス(-webkit-
や -ms-
)を自動で付けてくれるかどうか
*4: ビルド時に Auto Prefixer を挟む必要があります