69
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

CSS-in-JS ~初学者視点でどのライブラリが使いやすいかの比較~

Posted at

#はじめに ~CSS-in-JSの紹介記事を書こうと思った理由~
私は学生時代にほとんどプログラミングに触れてこなかった初学者です。
今も、会社の研修でプログラミングの課題を実施した程度のスキルですが、最近はWebアプリなどのWeb系のプログラミングに興味をもっています。

そんな私が研修の一環として技術調査をしているとき、CSS-in-JSというWebプログラミングの分野で話題の技術があると知り、今回調査してみました。
軽く調べてみると、CSS-in-JSはコンポーネント化して書くのが特徴で、管理するCSSが多い場合に役に立ちそうに思えました。
そこで、将来管理するCSSが増えることを見据え、初学者のうちからコンポーネント化して書く方法に慣れていた方が良いなと思い、この記事を執筆しています。

この記事では、CSS-in-JSやその代表ライブラリを紹介し、初学者の視点からどのライブラリが使い勝手が良く分かりやすかったかについて、CSSやJavaScriptをなんとな~く書いたことあるなあくらいの初学者に向けて話していきます。
比較した過程なんていいから、どのライブラリがおすすめだったのか知りたいという方はこちらをご覧ください~。

#CSS-in-JSが作られた背景
CSSは文書構造と見た目の表現を切り離すことができ、HTMLだけでは表現できないデザインやレイアウトが出来ます。このCSSを使って巨大なwebサイトを作る場合、不都合な点がいくつもあります。

1.CSSが増えると管理がしにくくなる
2.CSSが増えるということは容量も大きくなるのでダウンロードするにも時間がかかる
3.作成したCSSの一部分だけを変更したくても関係する要素がすべて変更され、意図しない部分のレイアウトが崩れてしまう可能性がある

そこで、部品ごとに定義し組み合わせて一つの機能を作り上げていく「コンポーネント化」をして書くべきという考えが生まれました。
コンポーネント化して書くのに使用するフレームワークやライブラリ上におけるスタイリング方法として出来たのがCSS-in-JSです。

#CSS-in-JSとは
JavaScript コード内にスタイルに関する記述を含めてしまおうというアプローチで、コンポーネント(構成要素)ごとに考えることが出来る技術です。
実行時にスタイル(CSSクラス)を生成し、DOM(処理を行う部分)に紐付けされます。

#CSS-in-JSのメリットとデメリット
CSSとCSS-in-JSを比較したときのメリットとデメリットについて挙げます。

###メリット

  • JavaScriptとCSSで変数・関数が同じものを使える
  • class名を深く考える手間が減る
  • 後から変更したいときに修正しやすく、ファイルのメンテナンス性が良い
  • JavaScriptでスタイル宣言を管理することによって未使用の宣言ブロックを検出しやすくするため、ESLintやTypeScriptを使用すれば、デザインの変更時などに不要になったスタイルを確実に取り除きやすくなった

    (CSSファイルから未使用のスタイルを検知するツールはあるが、CSSファイルで未使用と判断されても、JavaScriptで使われているなど、CSSとJavaScriptで統制がとれていないこともある)
  • スコープのないCSSにスコープを付与できる
  • コンポーネントのスタイル設定が楽になる
  • セレクタ(CSSを適用させたいHTML要素を指定するためのもの)との関わり方をCSSだけで実現できなければならないという制約がない
  • React.jsでは、生成されたコンポーネントに対してProps経由でJavaScriptの値を渡すことができるため、ちょっとしたスタイルの変更をする場合に手間が少なくて済む
    ※propsとは React.jsで、コンポーネントがデータを受け取ったり、表示したりするための主要な方法の1つで、親コンポーネントから渡されたプロパティのこと。
    簡単にいうと、それぞれのコンポーネントが持つ情報のことで再利用性が高い状態にするためのもの。

###デメリット

  • CSS-in-JS の環境が整っていない
    • 対応したエディタ/プラグインが提供されているエディタ上でなければ、シンタックスハイライトやコード補完がまったく効かず、ひたすら単色の文字列を編集していくことになる。
    • CSSを記述する場合に使用する開発支援ツールは、純粋な .css ファイルを前提に開発されている。そのため、使用するCSS-in-JSのライブラリとの相性を検証しなければいけない 。
      対応していた場合であっても、追加で別パッケージを導入したり、開発支援ツールの設定の変更が生じたりする可能性がある。
  • CSS-in-JSは実行時にスタイルを生成する仕組みのため、短時間の間に大量のスタイルを生成することになり、パフォーマンスの低下を招く。
    • ただ、そもそも大量のスタイルを扱う場合、headでCSSファイルの読み込みを行うため、表示に時間がかかる。それを考えるとあまりデメリットではないのでは?とも思います。

#CSS-in-JSのライブラリの内容比較

###ライブラリごとの最新更新日と記事数

ライブラリごとの最新更新月とQiita記事数、作成日、URLをまとめて最新更新月順に並べました。
他にもライブラリはたくさんありますが、今回調査したものは以下のライブラリのみです。

ライブラリ名 最新
更新月
Qiita 記事数 Github Star数 git
レポジトリ
作成日
 URL 
styled-components 2020/08 318 31k 2016/8/16 https://styled-components.com/
emotion 2020/08 111 11.7k 2017/5/27 https://github.com/emotion-js/emotion/releases/tag/%40emotion%2Fcore%4010.0.35
jss 2020/08 40 6k 2014/10/13 https://cssinjs.org/
fela 2020/08 2 1.8k 2017/6/8 https://github.com/robinweser/fela
aesthetic 2020/07 0 163 2018/8/26 https://github.com/aesthetic-suite/framework
aphrodite 2020/03 13 5.1k 2015/10/7 https://github.com/Khan/aphrodite
csx 2019/08 6 84 2015/11/28 https://github.com/typestyle/csx#readme
styletron 2019/05 2 3.1k 2016/4/14 https://github.com/styletron/styletron
glamorous 2018/12 9 3.7k 2017/4/4 https://github.com/paypal/glamorous
glam 2017/06 3 483 2017/4/26 https://github.com/threepointone/glam
rockey 2017/05 0 95 2017/3/21 https://github.com/tuchk4/rockey
j2c 2017/04 0 166 2015/5/17 https://github.com/j2css/j2c
glamor 2016/12 11 3.6k 2016/7/19 https://github.com/threepointone/glamor
(※2020年8月時点)

今回の記事では、最新更新月が今年であり、かつQiitaに記事があった5つのライブラリ(styled-components、emotion、jss、fela、aphrodite)について実際に触れてみて比較し紹介します。

###ライブラリごとのダウンロード数
5つの各ライブラリにおける直近の1年間(2019/09~2020/09)のダウンロード数は以下のグラフの通りです。

1year.png
(引用:npmtrends )
      
やはり1番多いのはstyled-componentsでした。記事数も多いので人気なのかなと思います。
次にjss、emotion、aphroditeでこの中で1番少なかったのはfelaでした。
emotionとjssを比較すると、Qiitaの記事数やGithub Star数はemotionの方が2倍くらい多いのに、ダウンロード数はjssの方が2倍近く多いのは、jssのみプラグインをサポートしていて拡張性が高いからでしょうか、、、。
全てのライブラリが年末にダウンロード数が落ち込んでいるのも面白いです。(原因調べたのですが、分からなかったので、理由分かる方コメント待ってます。)

###ライブラリごとの作られた背景と特徴(出来ることと出来ないこと)
5つのライブラリにおいて、そのライブラリが出来た背景とその特徴を紹介します。

ライブラリ名 背景 特徴
styled-components ReactコンポーネントシステムのスタイリングのためにCSSをどのように強化できるかを考えて作られた ・特定のフレームワーク(React)に依存する
スタイリングを目的とした要素を用意する必要がある(スタイルは使い回しでタグの要素だけ変更したい場合も都度新しい要素を作成することになる)
・実行時にCSSを使用してテンプレート文字列を解析する
・動的スタイルを更新すると新しいCSSルールを生成する
利用者が多いので情報を得やすい
emotion 他の多くのCSS-in-JSライブラリ(styled-componentsなど)に基づいて構築された ・特定のフレームワークに依存しない
・後発なこともあり、多機能で他ライブラリの機能の多くを更に洗練させた上で備えている
・styled記法のようにstyleを与えるコンポーネントタグを生成するだけではなく、css記法(CSSのタグ内でクラス名を指定)を使うこともできる
・動的スタイルを更新すると新しいCSSルールを生成する
利用者が多いので情報を得やすい
jss Sassを使わずにJavaScriptを使用してスタイルを記述し、また、グローバルスコープでクラスに名前をつける方法を考えなくてもいいようにするために作られた

※Sass : CSSを効率的に書くことができるメタ言語(CSSプリプロセッサ)
・特定のフレームワークに依存しない
・コア、プラグイン、フレームワーク統合など複数のパッケージで構成されている
・CSSの更新を非常に効率的に処理するため、複雑なアニメーションを作成できる
プラグインをサポートしている(公式プラグインが多数ある)
・動的スタイルを更新すると既存のスタイルを更新する
aphrodite サーバーサイドレンダリング、ブラウザープレフィックス、最小限のCSS生成をサポートするライブラリとして作られた ・特定のフレームワークに依存しない
・他のライブラリに比べると記述の自由度が低い(className以外へのCSS適応を許さないし、子要素のCSSを指定する方法も限定されている)
・単純なアニメーションの作成をすぐにできる
・ネストされたクラスをオーバーライドするソリューションが十分にサポートされていない
fela 多数の構成オプションといくつかのAPIを提供することによって、単一のユースケースを全て満たせるようにする目的で作られた ・特定のフレームワークに依存しない
・ユニバーサルレンダリング機能(1つのアプリケーションを構成するコードの大部分をサーバーとブラウザーの両方で実行できること)が付属されている
・小さくシンプルに作られているので拡張するためにプラグインを使用する必要あり

#それぞれのライブラリを実際に書いてみて比較

実際に5つのライブラリを使って読んだり書いたりしてみました。
CSS-in-JSを使う作業スペースとしてCSS in JS Playgroundを利用して、5つのライブラリで下の画像のスタイルを表現する際の書き方を比較しました。
css1.png
このEmailとPhone Numberの枠内に入力すると
css.png
上の画像のようにSUBMITボタンの色が灰色から青色に変わる動的処理も含みます。

ここでは、このプレイグラウンドで各ライブラリにおいて分かりやすかった部分、分かりにくかった部分、コード量や、5つのライブラリを通して理解しやすいと感じた理由とその部分のコードを、比較に使う部分のみ抜粋して紹介します。
 

###それぞれのコード量、分かりやすい部分や分かりにくい部分について
それぞれのライブラリごとに、コードの量や分かりやすかった部分や分かりにくかった部分を紹介していきます。

####styled-components
コード量は少なめ。
画面を最終的に生成する部分(コンポーネントを並べる処理)が分かりやすいです。

styledcomponents
export default function Login() {
  return (
    <Container>
      <Header />
      <Stripe />
      <Form fields={['email', 'phoneNumber']} />
    </Container>
  );
}

renderという1つの処理の中で、stateとpropsの両方を利用しているため、propsでの処理を理解するまでは分かりにくかったです。
※state : コンポーネント自身の状態変化を扱う。例えば、プルダウンの開閉状態など、コンポーネントごとに独立した状態を変化させるとき利用する。
※props : 親コンポーネントから引き継がれた値を扱う。例えば、親子関係のあるコンポーネントで、子を親の状態に依存して変化させるとき利用する。

styledcomponents

  render() {
    return (
      <Form onSubmit={this.handleSubmit}>
        {this.props.fields.map(fieldName => (
          <Input
            type="text"
            name={fieldName}
            placeholder={fieldName === 'email' ? 'Email' : 'Phone Number'}
            onChange={this.handleInputChange()}
            value={this.state[fieldName]}
            key={fieldName}
          />
        ))}
        <ButtonContainer>
          <Button onClick={this.handleReset()}>Reset</Button>
          <SubmitButton disabled={!this.state.valid}>Submit</SubmitButton>
        </ButtonContainer>
      </Form>
    );
  }


####emotion コード量は少ない。 styled-componentsと書き方が似ていて、画面を最終的に生成する部分(コンポーネントを並べる処理)が分かりやすいです。
emotion
export default function Login() {
  return (
    <Container>
      <Header />
      <Stripe />
      <Form />
    </Container>
  );
}

renderの中で、状態の保持がstateに統一されているので、個人的に分かりにくい部分はなかったです。

emotion

  render() {
    return (
      <Form onSubmit={this.handleSubmit}>
        {this.state.fields.map(fieldName => (
          <Input
            type="text"
            name={fieldName}
            placeholder={fieldName === 'email' ? 'Email' : 'Phone Number'}
            onChange={this.handleInputChange()}
            value={this.state[fieldName]}
            key={fieldName}
          />
        ))}
        <ButtonContainer>
          <Button onClick={this.handleReset()}>Reset</Button>
          <SubmitButton disabled={!this.state.valid}>Submit</SubmitButton>
        </ButtonContainer>
      </Form>
    );
  }


####jss プラグインを使用して拡張したり、jssを使うためにset upを書く必要があるため、コード量が増えます。
jss
const { classes } = jss.createStyleSheet(styles).attach();
jss
jss.setup(preset());

今回の比較では分かりやすい部分を見つけられなかったです。
ただ、プラグインの種類が多いので、自分が表現したいスタイルに必要な機能を、簡単に導入することができそうです。
renderの中身が長く、タグ内でクラス名を指定するため、処理部分が分かりづらいです。

jss
render() {
  const { valid } = this.state;                   
  return (
    <form className={classes.form} onSubmit={this.handleSubmit}> 
      {this.state.fields.map(fieldName => (
        <input
          type="text"
          name={fieldName}
          className={classes.input}
          key={fieldName}
          placeholder={fieldName === 'email' ? 'Email' : 'Phone Number'}
          onChange={this.handleInputChange()}
          value={this.state[fieldName]}
        />
      ))}
       
      <div className={classes.buttonContainer}>
        <button className={classes.button} onClick={this.handleReset()}>
          Reset
        </button>
        <button
          type="submit"
          className={[classes.button, valid && classes.buttonEnabled].join(
            ' '
          )}
          disabled={!valid}
        >
          Submit
        </button>
      </div>
    </form>
  );
}

####aphrodite 複数のコンポーネントに同じスタイルを適用するときにStyleSheet.createと記述しないといけなかったり、クラス名の指定をcss(styles.container)と書くためコード量は増えます。
aphrodite
const styles = StyleSheet.create({
 container: {
  ...
  }
});
aphrodite
  <div className={css(styles.stripe)} />

renderの中身が長く、タグ内でクラス名を指定するため、処理部分が分かりづらいです。

aphrodite
render() {
  return (
    <form className={css(styles.form)} onSubmit={this.handleSubmit}>
      {this.state.fields.map(fieldName => (
        <input
          className={css(styles.input)}
          type="text"
          name={fieldName}
          placeholder={fieldName === 'email' ? 'Email' : 'Phone Number'}
          onChange={this.handleInputChange()}
          value={this.state[fieldName]}
          key={fieldName}
        />
      ))}
       
      <div className={css(styles.buttonContainer)}>
        <button className={css(styles.button)} onClick={this.handleReset()}>Reset</button>
         
        <button
          type="submit"
          className={css(
            [styles.button].concat(
              this.state.valid ? [styles.buttonEnabled] : []
            )
          )}
          disabled={!this.state.valid} >Submit</button>
         
      </div>
    </form>
  );
}

####fela
小さくシンプルに作られているので拡張するためにプラグインを使用するコードが必要なため、コード量はやや増えます。

fela
const renderer = createRenderer({        
  plugins: [...webPreset]
})

画面を最終的に生成する部分(コンポーネントを並べる処理)が分かりやすいです。
上記コードのcreateRendererでスタイルをレンダリングをするためのメソッドを作成し、以下のコードのrendererでレンダリングされた全てのスタイルを処理しています。

fela
export default function(){
  <Provider renderer={renderer}>
  <Container>
    <Header />
    <Stripe />
    <Form />
  </Container>
  </Provider>
};

felaは特にユーザーが少ないので情報を調べるのが大変でした、、。

###初学者の自分にとって書きやすく、読みやすかったライブラリ

5つのライブラリを見てみて、初学者の自分にとって分かりやすいと感じたライブラリの共通点が2つありました。

  1. 画面を最終的に生成する部分(コンポーネントを並べる処理)がコンポーネント名
     コンポーネント名で並べているライブラリ(styled-components, emotion, fela)の方が分かりやすかったです。
      jssやaphroditeのようにクラス名を指定する書き方だと、完成形がどうなるのか分かりにくいので読みにくいと感じました。
jss
export default function Login() {
  return (
    <main className={classes.container}>
      <Header />
      <div className={classes.stripe} />
      <Form />
    </main>
  );
}
aphrodite
export default function Login() {
  return (
    <div className={css(styles.container)}>
      <Header />
      <div className={css(styles.stripe)} />
      <Form />
    </div>
  );
}

 

2.動的な処理(項目を入力するとsubmitボタンの色が変わる処理)の要素指定がスタイル側
 styled-componentsでは

styledcomponents
const SubmitButton = styled(Button).attrs({                              
  type: 'submit'
})`
  background-color: ${props => (props.disabled ? '#BBB' : '#6772e5')}; 
`;

 emotionでは

emotion
const SubmitButton = styled(Button)`
  background-color: ${props => (props.disabled ? '#BBB' : '#6772e5')};
`;
 
SubmitButton.defaultProps = {
  type: 'submit'
};

のようにコンポーネントの定義の部分で要素指定を記述します。
そのため、renderでは以下のようにシンプルに記述できます。

styledcomponents-emotion
render() {
  ...
  <SubmitButton disabled={!this.state.valid}>
   Submit
  </SubmitButton>
  ...
}

一方で、jssやaphrodite、felaではスタイル側ではボタンの色やサイズのみ定義し、render内で動的処理の要素指定が書かれます。
jss
render() {
  ...
  <button
   type="submit"
   className={[classes.button, valid && classes.buttonEnabled].join(' ')}
   disabled={!valid}>
   Submit
  </button>
  ...
}
aphrodite
render() {
  ...
 <button
  type="submit"
  className={css([styles.button].concat(this.state.valid ? [styles.buttonEnabled] : []))}
  disabled={!this.state.valid}>
  Submit
</button>
 ...
}
fela
render() {
 ...
 <button
  type="submit"
  className={[styles.button].concat(valid ? [styles.buttonEnabled] : [])}
  disabled={!valid}>
  Submit
 </button>
 ...
}

#まとめ
ライブラリを比較したときに1番おすすめしたいのはemotionでした。(初学者である自分の視点から比較した個人的な意見ですが。。)
その理由は

  1. 最終的に画面を生成する処理で、タグ名とコンポーネント名が同じところが分かりやすかったこと。
    これが1番大きいです。また、styled-componentsとfelaもタグ名とコンポーネント名は同じで画面を生成する処理部分は分かりやすいですが、emotionを1番に選んだことには他に理由があります。
  2. styled-componentsではCSS記法をスタイル追加出来ません。
    今回使用したワークスペースでは使用されていませんが、emotionではObject Styles機能を使うと以下のコードのようにCSS記法も出来ます
emotion-css.js
import { jsx, css } from '@emotion/core'
const hotpink = css({
  color: 'hotpink'
})
render(
  <div>
    <p css={hotpink}>This is hotpink</p>
  </div>
)

(参考:emotion公式サイト)

3.felaはここの2.で書いたようにボタンタイプをrender内で記述するために処理部分が分かりづらいです。

上に挙げた、コードを使用する上での3つの分かりやすかった点もそうなのですが、利用者が多いほうなので情報が得やすかったこと、後発ライブラリなので多機能で他のライブラリの機能の多くを洗練させた上で備えていることもおすすめする大事な理由です。

また、主観ですが、それぞれのライブラリごとに私が感じたことについて表にまとめました。

ライブラリ名 理解しやすさ 情報の多さ コード量の少なさ どういう場面で使うべきか
styled-components 使うための情報を早くほしいとき
emotion CSS記法を使いたいとき
jss × 複雑なアニメーションの作成
aphrodite × × シンプルな機能のみのコードを書きたいとき
fela × × プラグインを使用して自分に合ったスタイルにしたいとき

CSS-in-JSという技術そのものに関して言うと、デメリットとして挙げられている、
「対応したエディタ上でなければシンタックスハイライトが効かない」
「純粋な .css ファイルを前提に開発されている開発支援ツールを使用するときには相性を検証しなければいけない」
という問題は、実際にデメリットとして感じるときがありました。
ただ、それ以上にCSSファイルの管理がしやすくなることや、コンポーネント化することによって修正しやすいことだけではなく読みやすくなることなどのメリットを多く感じました。
また、デザイナーとプログラマーで開発する場合、CSS-in-JSではJavaScriptでスタイル宣言を管理することによって未使用の宣言ブロックを検出できるため、デザインの変更時などに不要になったスタイルを確実に取り除けるようになり分業しやすくなりそうです。

調べてみて、やはりCSS-in-JSから得られる利点を多く感じたので、コンポーネント化した書き方に慣れておくことをおすすめしたいです。
(初学者としては、読みやすくなるメリットも嬉しいです!)
また、今回は5つのライブラリに焦点を当てましたが、ここで紹介したもの以外にもCSS-in-JSのライブラリはまだまだあるので、他のライブラリも今後使ってみたいです。

#参考文献

69
50
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
69
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?