by @Quramy 2016.05.31
メインサービスの開発ではAngularを使っていますが、 社内向けのツール等でReactも使っています
UI Component と呼ばれる物の特徴:
多くのJS FrameworkがUI Componentの開発をサポート
Card Componentを作ってみる
import React from "react";
import {render} from "react-dom";
import {Card} from "./card";
const title = "React and CSS";
render((
  <div>
    <Card title={title}>Hello, card component!</Card>
    <Card title={title} primary={true}>Hello, primary card!</Card>
  </div>
), document.getElementById("app"));
React.Component を使ってComponentを作る:
import React from "react";
import cx from "classnames";
export class Card extends React.Component {
  render() {
    const {title, primary, children} = this.props;
    return (
      <div className={cx('card', {primary: primary})} >
         <header className="title">{title}</header>
         <div className="body">{children}</div>
      </div>
    );
  }
}
CSSはこんな感じ?
.card {
  background-color: #fff;
  padding: 20px;
  border: 1px solid #f0f2fb;
  border-radius: 3px;
  box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16);
  margin-bottom: 30px;
}
.card .title {
  font-size: 18pt;
  margin-bottom: 10px;
}
.card.primary {
  background-color: #62c4a4;
  border: none;
  color: white;
}
.card.primary .title {
  font-weight: bold;
}
...本当にこれで良いのか?
Style定義はとても壊れやすい
.card {
  background-color: #fff;
  padding: 20px;
  /** 中略 **/
}
/** これを追記すると... **/
div .card {
  background-color: red;
}
/* Card Componentの内部利用のために定義された CSS Class */
.card .title { font-size: 18pt; }
// Card Componentの利用コード
import { Card } from "./Card";
function renderCard() {
  return (
    <Card>
      <sectoin className="sub">
        {/* 利用者側はCardがtitleというclass名を使っていることを知らない */}
        <div className="title">sub title</div>
      </sectoin>
    </Card>
  );
}
<!-- 出力されるDOM -->
<div class="card">
  <header class="title"></header>
  <div>
    <sectoin class="sub">
      <!-- Cardが内部的に使っている .card .title のルールが適用されてしまう -->
      <header class="title">sub title</header>
    </sectoin>
  </div>
</div>
ここまでの結論:
React.Componentを使っただけで CSSが"適切にカプセル化"される訳ではない
CSSセレクタがGlobalであることが最大の問題
CSSのGlobal性と戦うための武器:
今回はCSS in JSとCSS Modulesを中心に紹介
CSS in JSではStyleとなるオブジェクトを直接JSで記述する.
/* CardStyles.js */
export const root = {
  backgroundColor: "#fff",
  padding: "20px",
  border: "1px solid #f0f2fb",
  borderRadius: "3px",
  boxShadow: "0 2px 5px 0 rgba(0,0,0,0.16)",
  marginBottom: "30px",
};
// 以下略
/* Card.jsx */
import React from "react";
import * as st from "./CardStyles";
export class Card extends React.Component {
  render() {
    const {title, primary, children} = this.props;
    return (
      { /* inline style に展開される */ }
      <div style={primary ? st.primaryRoot : st.root}>
        <header style={primary ? st.primaryTitle : st.title}>{title}</header>
        <div>{children}</div>
      </div>
    );
  }
}
CSS class名を介さずにinline-styleを利用することで, StyleをComponentに閉じ込めることができる
JavaScriptの関数を使えば、Styleの合成も容易
export const root = {
  backgroundColor: "#fff",
  padding: "20px",
  border: "1px solid #f0f2fb",
  borderRadius: "3px",
  boxShadow: "0 2px 5px 0 rgba(0,0,0,0.16)",
  marginBottom: "30px",
};
export const primaryRoot = Object.assign({}, root, {
  backgroundColor: "#62c4a4",
  border: "none",
  color: "white",
});
Styleを.jsで管理するメリット:
inline-styleに展開する場合、下記が利用できない
2016年5月現在, 最も人気のあるCSS in JSライブラリ (~3,000 )
:hover等、いくつかの擬似クラスセレクタをサポート
React NativeのようにStyleSheet.create を使ってjsからStyleSheetを生成
styleやComponent名(Flex等)を静的解析して.cssを生成
参考: React + CSS in JS の主要なツールの比較:
https://github.com/FormidableLabs/radium/blob/master/docs/comparison/
Example of keyframes animation with Radium
Chromeの開発者ツールでstyle属性を見てみよう
import React from "react";
import Radium from "radium";
@Radium
export class AnimatedCircle extends React.Component {
  render() {
    const { delay } = this.props;
    return (
      <div style={[styles.root, animation(delay)]}>
        <div style={styles.movable}>
          <div style={styles.circle}></div>
        </div>
      </div>
    );
  }
}
Class Decorators(ES.next)を ComponentのClassに付与
styleに配列が使えるように拡張される
styleオブジェクトに":hover"が利用可能に
const height = "50px";
// アニメーションの定義
const translationKeyframes = Radium.keyframes({
  "0%":   {transform: "translateX(0%)"},
  "50%":  {transform: "translateX(100%)"},
  "100%": {transform: "translateX(0%)"},
}, "pulse");
const animation = (delay) =>({
  animation: `x 2.5s ease ${delay} infinite`,
  animationName: translationKeyframes,
});
const styles = {
  root: {
    position: "relative",
    width: "100%", height,
    marginBottom: "15px",
    ":hover": { /* :hover時のstyle */
      opacity: 0.6,
    },
  },
  movable: {
    position: "absolute",
    width: "100%",
    top: 0, bottom: 0,
  },
  circle: {
    backgroundColor: "#62c4a4",
    width: height, height,
    borderRadius: "25px",
  }
};
Radium.keyframesでアニメーションを定義
':hover'をkeyとしてstyle定義
import React from "react";
import ReactDom from "react-dom";
import { AnimatedCircle } from "./AnimatedCircle";
import { StyleRoot } from "radium";
ReactDom.render(
  <StyleRoot>
    <AnimatedCircle delay="0s"></AnimatedCircle>
    <AnimatedCircle delay=".3s"></AnimatedCircle>
    <AnimatedCircle delay=".6s"></AnimatedCircle>
  </StyleRoot>
, document.getElementById("app"));
keyframesを利用するためにはStyleRootでラップするが必要ある
Glen Maddern が2015年に提唱
Just look at the range of approaches to handling :hover states among the projects I referenced earlier, something that has been solved in CSS for a long time. So, while we’re bullish about our approach and firmly defend the virtues of CSS .
CSS in JSが.jsでstyleを生成するのに対して CSS Modulesは.cssを.jsから利用する
CSS Modulesでは定義されたClassがES 2015 Modulesのように振る舞う
/* Card.css */
.title { font-size: 18pt; }
/* Card.css.js */ export const title = "title";
冒頭のCard ComponentをCSS Modulesで書き換えてみる
/* src/Card.css */
.root {
  background-color: #fff;
  :
}
.title {
  font-size: 18pt;
  :
}
/* src/Card.jsx */
import React from "react";
import * as st from "./Card.css"; // .cssからimport
export class Card extends React.Component {
  render() {
    const {title, children} = this.props;
    return (
      <div className={st.root} > {/* class名がexportされている */}
         <header className={st.title}>{title}</header>
         <div>{children}</div>
      </div>
    );
  }
}
RenderingされたDOMを開発者ツールで見てみると...
CSS Class名が書き換えられているのが確認できる
CSS ModulesではModule bundlerのプラグインを利用する:
/* CSS Modules pluginが生成するmoduleイメージ */
module.exports = {
  "root": "_root_gub7p_1",
  :
};
css-modules/css-modulesify pluginが利用可能
npm i browserify babelify css-modulesify browserify \ -t [ babelify ] \ -p [ css-modulesify -d dist/bundle.css ] \ -d dist/bundle.js
NODE_ENV の値によって, 生成されるCSS Class名が異なる:
デフォルト: prefixにlocal file pathが付与される
production: suffixにfile hash付与される
webpack公式のcss-loaderから利用可能 modules オプションでCSS Modulesとして解釈されるようになる
var path = require('path');
module.exports = {
  module: {
    loaders: [
      { test: /\.js$/, exclude: /node_modules/, loaders: ['babel-loader'] },
      // postcssと併用する場合, importLoaders=1が必須
      { test: /\.css$/, loaders: ['style', 'css?modules&importLoaders=1', 'postcss'] },
    ],
  },
  postcss: [
    require('autoprefixer'),
  ],
  entry: './src/index.js',
  output: { path: path.resolve('dist'), filename: 'bundle.js' },
  resolve: { extensions: ['', '.js'] },
};
composes キーワードでCSS Class同士の合成が可能
(Sassの@extend, CSS @apply rule相当)
/* util.css */
.large {
  font-size: 18pt;
  margin-bottom: 10px;
}
/* Card.css */
.title {
  composes: large from './util.css';
  font-weight: bold;
}
/* generated module of Card.css */
module.exports = {
  "title": "title_xxx_yyy large_zzz_www"
}
@value で値を定義すると.js, .cssからimport出来る
/* shared.css */ @value duration 0.6s;
/* animatedBall.css */
/* aliasを付けてimport可能 */
@value duration as bounceDuration from "./animation-values.css";
@keyframes bounce {
  33% { transform: translateY(-20px); }
  66% { transform: translateY(0px); }
}
.bounce {
  animation: bounce bounceDuration infinite ease-in-out;
}
css-modules/postcss-modules を利用することでCSS Modulesの事前変換が可能
/* styles.css */
.myClass {
  color:red;
}
/* converted_styles.css */
._myClass_xxx_yyy {
  color:red;
}
// converted_styles.json
{
  "myClass": "_myClass_xxx_yyy"
}
出力されたjsonから対応関係を参照することで他言語(e.g. Ruby)からもCSS Modulesを利用できる
CSS in JSとCSS Modules, どっちを使えばいいの?
現状、優劣は付け難い
"styleを.jsで書きたいか" or "styleを.cssで書きたいか" 天秤に掛けて決めましょう
TypeScriptでもCSS Modulesが使いたい
import * as React from "react";
import * as st from "./Card.css"; // compile errorに...
export interface CardProps { title: string; primary?: boolean}
export class Card extends React.Component<CardProps, {}> {
  render() {
    const {title, primary, children} = this.props;
    return (
      <div className={primary ? st.primaryRoot : st.root} >
         <header className={primary? st.primaryTitle : st.title}>{title}</header>
         <div>{children}</div>
      </div>
    );
  }
}
CSS Modulesから.css.d.tsを生成するツールを作りました
/* Card.css */
.root { ... }
.title { ... }
// Card.css.d.ts export const root: string; export const title: string;
BEM(Block Element Modifier)について: