DMMグループの一番深くておもしろいトコロ。
テクノロジー

堅牢なCSSをReactに手軽に実装できるstyled-jsx

DMMグループの一番深くておもしろいトコロ。

はじめに

こんにちは、クリプトマイニング事業部でエンジニアをしているusagi-fです。フロントエンドを中心としてデザインやサーバーサイドも触っています。

今回はReactを使ったアプリケーション上でCSS実装をするために便利な「styled-jsx」というライブラリを紹介していきます。

(本記事は、Reactの実装環境についての知識が一定以上あることを前提としています。ご了承ください。)

概要

styled-jsxはHyperNowNext.jsなどを提供しているzeitによるライブラリです。主にReactを使用したプロダクトにおいて、JSXに対してスコープ付きのCSS実装をサポートします。

通常、React自体にはCSSを記述するための標準的な実装はされておらず、自身でCSSをどのように組み込むかを考えることになります。従来のようなHTMLに対するクラス命名ルールによるCSS設計を用いることも一つの解決策ですが、UIコンポーネント自身の設計思想に沿って実装させていくことがこれからの主流になるでしょう。噛み砕いて説明すると、これまでバッティングが起こらないように綿密に設計していたCSSの決まりごとを、ライブラリ導入によって機械的に実現できるようになります。

このような、JavaScriptを経由してCSSを実装していくアプローチは「CSS in JS」とも呼ばれ、有名なライブラリとしては「styled-components」が挙げられます。実現できることはそれとほとんど同じですが、今回はまだ日本語の解説記事が少ないstyled-jsxを取り扱いたいと思います。

styled-jsxの特徴として、わかりやすいものを一部抜粋します。

  • CSSの機能をフルサポート
  • 3KBと非常に軽量(gzip前は12KB)
  • ベンダープレフィックス組み込み済
  • ソースマップ対応

軽量で高速でありつつも、実装における最低限の機能はキチンとサポートされています。便利にCSSを書いていきたいけれどあまり仰々しい作りにはしたくない、という状況においてはかなり現実的な選択であると言えるでしょう。

導入と実装

インストール

$ npm install --save styled-jsx
{
  "plugins": [
    "styled-jsx/babel"
  ]
}

一般的なJavaScriptライブラリと同様に、npmコマンドを利用してインストールしていきます。また、トランスパイル時のプラグインとして動作するように、Babelの設定に追記をします。必要な前準備は以上です。びっくりするほど簡単ですね。

オプション

styled-jsxに複雑なオプションはありませんが、ベンダープレフィックスやソースマップを意図的に制御したい場合には活用できます。

{
  "plugins": [
    ["styled-jsx/babel",
      {
        "vendorPrefixes": false,
        "sourceMaps": false
      }
    ]
  ]
}

簡単な実装例

それでは実際に、React実装されたコンポーネントに記述してみます。<style jsx>というタグをJSX上で利用することになります。JSXはHTMLやXMLを書くように直感的にDOM宣言ができる拡張構文ですが、styled-jsxではHTMLの中にstyleタグでCSSを記述する時とまったく同じ感覚で実装できてしまうことが最大の強みでしょう。

テンプレートリテラルを使っており、純粋なCSSのシンタックスで記述することができる点も大きな特徴の一つです。

export default () => (
  <div>
    <p>Hello World!</p>
    <style jsx>{`
    p {
      color: red;
    }
    `}</style>
  </div>
)
<!-- 出力結果 -->
<head>
  <style type="text/css" data-styled-jsx>
    p.jsx-2579517479{color:red;}
  </style>
</head>
<body>
  <div class="jsx-2579517479">
    <p class="jsx-2579517479">Hello World!</p>
  </div>
</body>

該当のタグ内に書かれたCSSは、ビルド後にブラウザで確認すると、head要素内へ抽出されて出力されることになります。そして、styled-jsxは記述したコンポーネントをスコープとして自動的に展開します。ルートにあたる要素を含むコンポーネント内のあらゆる要素に、ランダムな数列によるクラスが自動的に付与されているのがわかります。共通のコンポーネント内に存在するため、同じクラス名が付いてスタイル汚染が防がれるようになっています。

複数の構成要素による出力結果

もう少しだけ複雑なコンポーネント構成での挙動を見てみます。以下のようにさらに複数の要素があっても、同じように割り振られているのが見て取れます(数列は変わっているが、同じものが付与されている)。しかし、別のコンポーネントを読み込んでいる場合は、スコープが異なるため別のCSSが適用されるようになるのも確認できます。

export default () => (
  <div>
    <p>Hello World!</p>
    <em className="foobar">foobar</em>
    <Children />
    <style jsx>{`
    p {
      color: red;
    }
    .foobar {
      color: blue;
    }
    `}</style>
  </div>
)
<!-- 出力結果 -->
<head>
  <style type="text/css" data-styled-jsx>
    p.jsx-987215791{color:green;}
  </style>
  <style type="text/css" data-styled-jsx>
    p.jsx-2825995{color:red;}
    .foobar.jsx-2825995{color:blue;}
  </style>
</head>
<body>
  <div class="jsx-2825995">
    <p class="jsx-2825995">Hello World!</p>
    <em class="jsx-2825995 foobar">foobar</em>
    <p class="jsx-987215791">Children Component</p> <!-- ここだけ異なるスコープのCSSが適用される -->
  </div>
</body>

また、スタイリング用の要素をJSX内に組み込まなければならなくなるため、どうしても不要なdiv要素が増えてしまいがちな仕様ですがReact.Fragmentを使うとDOM構造をきれいに保つことができます。

import React, { Fragment } from 'react'

const Children = () => {
  return (
    <Fragment>
      <p>Children Component</p>
      <style jsx>{`
      p {
        color: green;
      }
      `}</style>
    </Fragment>
  )
}

便利に記述する

CSSの注入

非常に便利に機能することがわかりましたが、JSXの内部に生のCSSを書いていくため複雑な表現を行うUIの場合は記述量が膨大になりがちです。視認性・可読性の悪化に繋がることもあるため、できれば分離して記述したいと思うことがあるでしょう。そこでstyled-jsxではCSS記述のみを独立して定義し、変数のように扱うこともできます。

import css from 'styled-jsx/css'

const styles = css`
p {
  color: red;
}
`

export default () => {
  <div>
    <p>Hello World!</p>
    <style jsx>{styles}</style>
  </div>
}

これは、CSS定義のみを別のファイルへ移管できることを示しています。コンポーネントファイルをきれいに保つことに役立つでしょう。

import styles from './styles'

export default () => (
  <div>
    <p>Hello World!</p>
    <style jsx>{styles}</style>
  </div>
)

より管理しやすくするために、複数のオブジェクトとしてCSSを個別にExportして利用することもできます。ただし、レンダリング時に生成されるスコープは適用後のコンポーネントの単位で行われることに注意してください。

// styles.js
import css from 'styled-jsx/css'

export const button = css`button { color: hotpink; }`
export default css`div { color: green; }`
import styles, { button } from './styles'

export default () => (
  <div>
    <button>Click Me!</button>
    <style jsx>{styles}</style>
    <style jsx>{button}</style>
  </div>
)

ダイナミックスタイル

styled-jsxのようなCSS in JS形式の実装では、JavaScript上にある値を簡単にCSSの世界へ持ち込むことが可能になります。これにより今まで煩雑な実装が必要になっていた動的な見た目の変化を、より直感的に実現できます。

特にstyled-jsxではCSS記述はJavaScriptのテンプレートリテラルによって渡されるため、${}を使うだけで値の展開が実現できます。親コンポーネントからPropsなどを利用して簡単にスタイルの操作が行えることになります。

export default (props) => {
  return (
    <div>
      <p>Children Component</p>
      <style jsx>{`
      p {
        padding: ${ 'large' in props ? '50' : '20' }px;
        background: ${props.theme.background};
      }
      `}</style>
    </div>
  )
}
export default () => (
  <div>
    <Children theme={{background: 'red'}} large />
  </div>
)

グローバルスタイル

スコープを持ったCSSが定義できるようになりましたが、逆に元々のグローバルなCSSを作成したくなった場合に困ってしまいます。その場合にはglobalという属性値を与えてやることでスコープ文字列を付与させずにおくこともできます。使用頻度は少なそうですが、うまく活用すればリファクタリングにも大いに生かせます。

export default () => (
  <div>
    <p>Hello World!</p>
    <style jsx global>{`
    p {
      margin: 0;
    }
    `}</style>
  </div>
)
<!-- 出力結果 -->
<head>
  <style type="text/css" data-styled-jsx>
    p{margin:0;} /* グローバルCSSが定義できる */
  </style>
</head>
<body>
  <div class="jsx-1909914245">
    <p class="jsx-1909914245">Hello World!</p>
  </div>
</body>

ワンオフグローバルセレクタ

全体へ影響を与えるグローバルスタイルとは異なり、各セレクタに対し:global()という記述をサポートするものです。これを使うと、“ある特定のコンポーネント配下におけるグローバルセレクタ”を作成できます。難しく聞こえるかもしれませんが、今までにCSSのカスケーディング特性を使いこなしてきた人にとっては、これがとても便利なものであることは理解できると思います。制御可能な範囲の不特定多数要素に簡単にスタイル適用をしつつも、一定のスコープを保つことが実現できます。

以下の例では、このコンポーネントにぶら下がっているUIに限って、children-styleというクラスがグローバルに存在しています。クラスをカスタムできるように作っているコンポーネントであれば、グローバルスタイルを外から後付けできるという柔軟さを備えます。

(optionClassNameは、クラス名をカスタムできる独自に作成したPropsを指しています)

export default () => {
  return (
    <div>
      <Children optionClassName="children-style" />
      <style jsx>{`
      div :global(.children-style) {
        margin: 20px;
      }
      `}</style>
    </div>
  )
}
<!-- 出力結果 -->
<head>
  <style type="text/css" data-styled-jsx>
    div.jsx-1849707676 .children-style{margin:20px;}
  </style>
</head>
<body>
  <div class="jsx-1849707676">
    <p class="jsx-987215791 children-style">Children Component</p>
  </div>
</body>

まとめ

これまでCSSは言語の特性上、常に影響範囲を気にする必要がありました。しかし、スコープを機械的にコンポーネントと合わせてくれるライブラリの存在はこれまでのCSS設計論を覆すようなインパクトがあります。ですが、うまく使いこなすためにはJavaScriptの知識が必要になってきます。今後はCSSを書くためにJavaScriptの技術が重要になる世界になっていく可能性は大いにあると感じます。

styled-jsxは比較的これまでのCSSの形を崩さずに、ミニマムかつ柔軟に設計されているバランス感覚の良いライブラリだと思います。いろいろなAPIメソッドを覚える必要もなく、気軽に導入できて十分実務として利用できることでしょう。

似ているライブラリを比べた場合、備わっている機能には大きな差はないでしょう。最終的に気軽さや見やすさによってライブラリを選定することになるとしたら、個人的にはstyleタグ+テンプレートリテラルの組み合わせは気持ち良く書けるものに感じます。

宣伝

『React開発 現場の教科書』という私が書いた書籍が発売中です。本記事で扱った技術の前提知識が網羅されており、styeld-jsxについても軽く触れています。興味がある方は是非お手に取ってみてください。

 

シェア