react-css-note



react-css-note

0 1


react-css-note


On Github Quramy / react-css-note

How to style React Components

by @Quramy 2016.05.31

About me

倉見 洋輔 @Quramy

株式会社WACUL所属のフロントエンジニア

メインサービスの開発ではAngularを使っていますが、 社内向けのツール等でReactも使っています

Agenda

  • UI ComponentにおけるCSSの問題点
  • 解決方法1: CSS in JS
  • 解決方法2: CSS Modules
  • まとめ
  • おまけ(時間があれば)

What's Component ?

UI Component と呼ばれる物の特徴:

  • 見た目と振る舞いがまとめて提供されている
  • 適度にカプセル化されている
  • 再利用性が高い
  • 保守しやすい
  • etc...

How to create Component?

多くのJS FrameworkがUI Componentの開発をサポート

  • React: React.Component
  • AngularJS: angular.module(...).component(...)
  • Vue.js: Vue.component(...)
  • and more...

Thinking with example

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"));
  • propsにtitleとprimaryを渡せる
  • childrenが子要素にrenderされる

Create component with JSX

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>
    );
  }
}

Write CSS

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;
}

It's completed!

...本当にこれで良いのか?

CSS is so fragile

Style定義はとても壊れやすい

  • 外部からComponentのCSSを破壊する例:
.card {
  background-color: #fff;
  padding: 20px;
  /** 中略 **/
}
/** これを追記すると... **/
div .card {
  background-color: red;
}
  • ComponentのCSSが子要素を破壊する例:
/* 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であることが最大の問題

Weapon of Choice ?

CSSのGlobal性と戦うための武器:

  • CSS 命名規約(e.g. BEM)
  • Web Components(Shadow DOM)
  • CSS in JS
  • CSS Modules

今回はCSS in JSとCSS Modulesを中心に紹介

CSS in JS

CSS in JS

Christopher Chedeau が2014年に発表React: CSS in JS で一躍話題に

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",
});

Other pros

Styleを.jsで管理するメリット:

  • プロジェクトで共有する変数も.jsで共有可能
  • 不要コード検出(e.g. eslint: no-unused-vars)
  • 動的ロード(e.g. SystemJS)
  • and more...

Cons of inline-style

inline-styleに展開する場合、下記が利用できない

  • 擬似要素セレクタ(e.g. ::before)
  • 擬似クラスセレクタ(e.g. :hover)
  • メディアクエリ
  • @keyframes

Major tools in CSS in JS

  • FormidableLabs/radium:

    2016年5月現在, 最も人気のあるCSS in JSライブラリ (~3,000 )

    :hover等、いくつかの擬似クラスセレクタをサポート

  • js-next/react-style:

    React NativeのようにStyleSheet.create を使ってjsからStyleSheetを生成

  • petehunt/jsxstyle:

    styleやComponent名(Flex等)を静的解析して.cssを生成

参考: React + CSS in JS の主要なツールの比較:

https://github.com/FormidableLabs/radium/blob/master/docs/comparison/

Example of Radium

Example of keyframes animation with Radium

Chromeの開発者ツールでstyle属性を見てみよう

Component with Radium

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"が利用可能に

Style object with Radium

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定義

Wrap components by StyleRoot

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でラップするが必要ある

CSS Modules

CSS Modules

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 .

http://glenmaddern.com/articles/css-modules

CSS in JSが.jsでstyleを生成するのに対して CSS Modulesは.cssを.jsから利用する

Local scoped by default

CSS Modulesでは定義されたClassがES 2015 Modulesのように振る舞う

/* Card.css */
.title { font-size: 18pt; }
/* Card.css.js */
export const title = "title";
  • titleを利用する際は明示的にimportする
  • 他のファイルに同名のtitle が定義されていても干渉しない

How to use CSS Modules

冒頭の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名が書き換えられているのが確認できる

Using module bundler

CSS ModulesではModule bundlerのプラグインを利用する:

  • .cssファイルのClass名を書き換えてglobalな名前が分からないようにする
  • 同時に.jsで利用するkeyと実クラス名の紐付け情報を作成してModule bundlerに引き渡す
/* CSS Modules pluginが生成するmoduleイメージ */
module.exports = {
  "root": "_root_gub7p_1",
  :
};

With Browserify

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付与される

With webpack

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'] },
};

Class composition

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"
}

Sharing variables

@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;
}

Offline transformation

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を利用できる

まとめ

Which tools do I use ?

CSS in JSとCSS Modules, どっちを使えばいいの?

CSS in JS

  • 単一のJSファイルにテンプレート(JSX)とStyle定義を記述できる
  • JavaScriptの様々なツール(e.g. eslint)がStyleに利用可能
  • :hoverやメディアクエリにはライブラリ毎に記法が異なる

CSS Modules

  • CSS記法がそのまま利用できる
  • JavaScript以外の言語からも利用しやすい
  • Module bundler pluginやpostcss pluginによる変換が必須

現状、優劣は付け難い

"styleを.jsで書きたいか" or "styleを.cssで書きたいか" 天秤に掛けて決めましょう

Thank you!

https://github.com/Quramy/react-css-note

Aappendix

With TypeScript

  • TypeScript 1.6以降ではJSXのコンパイルがサポートされている (tsc --jsx=react)
  • Component名, Props名をCompile Checkしてくれる

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を生成するツールを作りました

Quramy/typed-css-modules

/* Card.css */
.root { ... }

.title { ... }

// Card.css.d.ts
export const root: string;

export const title: string;

Is BEM dead?

BEM(Block Element Modifier)について:

  • 新しいプロジェクトで積極的に採用する必然性は低い
  • CSS in JSやCSS Modulesが肩代わりしてくれるのは"B"の部分だけ
  • localなsytleやClassが"Element" or "Modifier"のどちらであるのか、命名規約を持っておくのは良いこと
How to style React Components by @Quramy 2016.05.31