Help us understand the problem. What is going on with this article?

styled-components(CSS in JS)をやめた理由と、不完全なCSS Modulesを愛する方法

styled-components

画像は styled-components ツライっていう顔です。

Angularのようにスタイリングまで面倒を見てくれるUIフレームワークならまだしも、Reactの場合はコンポーネントのスタイリング方法も自身で選択しなければいけません。CSSのスタイリング方法/設計はいくつか存在しますが、どれも一長一短で、やはり銀の弾丸は存在しません。スタイリング方法を選択可能なUIフレームワークは、この混沌とした選択肢の中から価値を見出す必要があるわけです。

僕はBEMによる人力CSS管理(Sass/Less/Stylus)から、 { fontSize: 14 } のようなJSオブジェクト形式のCSS in JS、 styled-components のようなTemplate Stringsを利用したCSS in JS、そしてCSS Modulesまで幅広く公私ともに利用してきました。特に styled-components は1年半利用しましたが、後半は血反吐を吐く思いでした

このような変遷を経て今はCSS Modulesに落ち着いていますが、 React初学者向けの記事でたまに見かける「Reactを使う場合は、 styled-components 一択」というフレーズに対して「待った」をかけたかった ので、僕が styled-components をやめた理由と、乗り換え先であるCSS Modulesの利点及び欠点をまとめておきます。記事後半にも書いてありますが、デメリットを承知の上で styled-components を利用するのも良し、CSS Modulesの辛さを知った上でCSS Modulesを導入するのも良し、手間を必要とするが安定するBEM的運用も良し、です。

おことわり

  • 本記事では、JSXを”Reactを利用するための記法”として記述しています1
  • 本記事では、CSS Modulesをcss-loaderで利用するものとして記述しています
  • styled-components を基本としていますが、Emotionなどの似たようなCSS in JSでも参考になるかもしれません
  • 内容は styled-components 5.0.1をベースとしたものであり、バージョンアップによって以下の欠点は解決されるかもしれません

styled-components をやめた理由

1. JSX以上にエディタを選ぶ

Reactを使う上で必要不可欠とも言える記法──JSXは、JavaScriptの記法をベースとしつつ、 React.createElement のようなAPIをXHTML風の記法でラップすることで、可読性の向上と実行結果の予測を容易にしてくれます。JavaScriptもXHTMLも昔から大きく変わらない言語であるため、React初学者に対するJSXの学習コストは比較的低いものでしょう。

styled-components も同様に、CSSという昔から変わらない言語の記法をベースとしつつ、JavaScriptの世界にその身を落とすことで、JavaScriptの柔軟性を活かしながらもスタイリングの学習コストを抑える理想的なアプローチに見えます。
しかし .js (または .ts )を拡張子としながらも、Template Strings内にCSSを直接書いていく styled-components は、それに対応したエディタ/それに対応したプラグインが提供されているエディタ上でなければ、シンタックスハイライトやコード補完がまったく効かず、ひたすら単色の文字列を編集していく地獄を見る ことになります。

今では、メジャーなIDE/軽量エディタではJSXのシンタックスハイライトが標準でサポートされていますが、 styled-components のようにJavaScriptファイル上でCSSのシンタックスハイライトを有効にできるエディタは多くありません。また、JavaScript上で別言語のシンタックスハイライトを有効にさせるという異質な環境から、エディタ自体の設計によってはそもそも対応できないエディタも存在し得ます。

2. CSS向けの開発支援ツールに相性問題が生じる

コンポーネント数が多いプロジェクトでCSSを記述する場合、stylelintなどの開発支援ツールを導入するプロジェクトは少なくないでしょう。これらの 開発支援ツールは純粋な .css ファイルを前提に開発されているため、 styled-components との相性を検証しなければいけません

幸いにも、CSSのリンターツールとしてデファクトスタンダードとも言えるstylelintは、 styled-components が公式にプラグインを提供しているため利用できなくはありませんが、一部のルールを使えなかったり、stylelint-orderなどのstylelintプラグインの挙動が安定しない……などの場面に出くわすでしょう。

styled-componentsのために利用を諦めたstylelintルールたち
# .stylelintrc.yml
rules:
  no-descending-specificity: ~ # Bugs exist in styled-components
  no-duplicate-selectors: ~ # Cannot use for dynamic selectors such as styled-components
  selector-type-no-unknown:
    - true
    - ignoreTypes:
        - "$dummyValue"
        - "/^\\-styled\\-mixin/"
  ...

エコシステムから少し離れてまでも、 styled-components を利用すべきかどうかを吟味する必要があるでしょう。

3. 必要な依存関係/設定が多い

1の通り、 styled-components でCSSのシンタックスハイライトやコード補完を有効にするには、それに対応したエディタ/プラグインが提供されているエディタを利用する必要があります。

開発支援ツールの変更例(.stylelintrc.yml)
+ processors:
+   - "stylelint-processor-styled-components"
+ extends:
+   - "stylelint-config-styled-components"
+ syntax: "scss"

また2で挙げたstylelintを利用したい場合、追加でstylelint-processor-styled-components及びstylelint-config-styled-componentsを導入しなければいけません。このように、開発支援ツールが styled-components に対応していた場合であっても、追加で別パッケージを導入する必要が生じます。パッケージの導入だけでなく、開発支援ツールの設定の変更なども生じる可能性があります。

ビルド設定の変更例(.babelrc.js)
  module.exports = {
    plugins: [
      ["@babel/plugin-proposal-decorators", { legacy: true }],
      ["@babel/plugin-proposal-class-properties", { loose: true }],
+     ["babel-plugin-styled-components", { ssr: true }],
    ],
  };

加えてビルド設定にも変更が必要です。 styled-components が生成するクラス名にはランダムなハッシュ値が利用されますが、開発環境でのデバッグ容易性を考えると、Babelプラグインbabel-plugin-styled-componentsを導入して displayName をクラス名に表示させるのは必須とも言えるでしょう。Next.jsなどに styled-components を組み込んで、クラス名を含むServer Side Renderingが必要な場面においても、このBabelプラグインは必要となります。

最後に、これは styled-components に限った話ではないものの、TypeScriptを利用するプロジェクトでは型定義ファイルが必要となるため、別途 @types/styled-components も必要となります。

4. styled-components の仕組みを理解しないと、パフォーマンスに影響を与えるコードが生まれる

styled-components を使えば、生成されたコンポーネントに対してProps経由でJavaScriptの値を渡すことができます。これは、 styled-components が実行時にスタイル(CSSクラス)を生成する仕組みだからこそ成せる技でもあります。

一方でこの 仕組みをよく覚えておかなければ、短時間の間に大量のスタイルを生成することになり、パフォーマンスの低下を招く ことになるでしょう。 onMouseMoveonTouchMove など頻繁に呼ばれるコールバック関数内で、 styled-components のPropsを呼ぶようなことは避ける必要があります。

type BoxProps = { x: number; y: number; };
const Box = styled.div<BoxProps>`
  width: 128px;
  height: 128px;
  background-color: red;
  transform: translate3d(${props => props.x}px, ${props => props.y}px, 0);
`;

export const Component = () => {
  // そもそも頻繁に更新される値を State で管理すること自体、 Bad React Way ではありますが……
  const [xy, setXY] = React.useState([0, 0]);
  const onMouseMove = React.useCallback(
    (event: React.MouseEvent<HTMLDivElement>) => {
      setXY([event.clientX / 10, event.clientY / 10]);
    },
    [],
  );

  return (
    <div>
      <Box x={xy[0]} y={xy[1]} onMouseMove={onMouseMove} />
    </div>
  );
}

Reactに限らず、上記コールバック関数内でLayoutレイヤーを操作するDOM APIをコールすることはご法度とも言えますが、 transform: translate() を始めとするCompositeレイヤーの操作はパフォーマンスへの影響が少ないため、例外的に許可されている動的スタイリングとも言えるでしょう。しかし変更対象プロパティが transform プロパティのみであったとしても、 styled-components 経由で動的スタイリングを行うことは避けるべきです。

幸いにも styled-components は、200以上のCSSクラスが生成された場合にWarningを表示してくれます。 styled-components を使うのであれば、開発中はWarningを見逃すことがないようにしておきたいところです。

5. エディタ上で、どれが styled-components か分からなくなる

styled-components は、内部に <div> のようなReact Elementを持つReact Componentを生成します。したがってJSXを見たとき、 開発者が自身で定義したReact Componentと styled-components によって生成されたReact Component、そしてReact Elementの区別がつかなくなります 。個人的には、これが脱 styled-components を決めた一番の理由となりました。

例えば以下のようなコードの場合、HTMLに日頃から見慣れているWebフロントエンドエンジニアにとって、どちらが読みやすいかは一目瞭然でしょう(あえてタイプセレクタを利用しない設計にしています)。

styled-componentsを利用した例
(
  <Header>
    <SiteName>Hello World</SiteName>
    <Navigation>
      <NavigationItem href="/">トップ</NavigationItem>
      <NavigationItem href="/about">当サイトについて</NavigationItem>
      <NavigationItem href="/contact">お問い合わせ</NavigationItem>
    </Navigation>
  </Header>
);
styled-componentsを利用せずクラスを利用した例
(
  <header className="header">
    <h1 className="siteName">Hello World</h1>
    <nav className="navigation">
      <a className="item" href="/">トップ</a>
      <a className="item" href="/about">当サイトについて</a>
      <a className="item" href="/contact">お問い合わせ</a>
    </nav>
  </header>
);

字面だけでなく、シンタックスハイライトのカラーリングからも区別がつかなくなってしまいます。現にQiitaのシンタックスハイライトも、 styled-components を利用したコードでは、緑一色になっています。

6. 外部コンポーネントのスタイリング方法に手間がかかる

import { ExternalHeader } from "external-components";

const FooComponent = () => {
  ...

  return <ExternalHeader className="???" />;
};

Reactに限らず、コンポーネント設計が一般的となった昨今では、UIコンポーネントの配布と利用が簡単になりました。使いたいUIコンポーネントをインストールし、ES Modules等で読み込み、JSXとしてコンポーネントを埋め込むだけです。

自身のプロダクト外で開発されたコンポーネントをスタイリングする代表的な方法の1つに、CSSクラス名をProps経由で渡すことが挙げられます。しかし styled-components は、クラス名を取得するAPIが用意されていないため、外部コンポーネントのスタイリングが難しく なります。

import { ExternalHeader } from "external-components";

const Wrapper = styled.div`
  .header {
    color: red;
  }
`;

const FooComponent = () => {
  ...

  return (
    <Wrapper>
      <ExternalHeader className="header" />
    </Wrapper>
  );
};

解決方法の1つとして、当該外部コンポーネントを囲う要素を用意し、その要素の子要素としてクラス名を付与することが挙げられます。多くの場面ではこの方法で問題ないものの、本来親要素が不要な場合でも親要素の作成を強いられる場面では、歯がゆさを感じられずにはいられません。また styled-components がExportしている css 関数を使うことで、スタイルを文字列として取得することはできるものの、style が生えたPropsは詳細度や小回りの観点からあまり標準的ではないため、使える場面は限られるでしょう。

不完全なCSS Modulesと付き合っていく道

僕はCSS in JSのメリットを感じつつも、日々ストレスとなっていた本記事で挙げたようなデメリットに限界を感じ、今ではCSS Modulesを利用してCSSを記述しています。 styled-components ほどの柔軟性はもちろんありませんが、それでも昔から語り継がれているスタイリングのパフォーマンスの知見をそのまま活かすことはできますし、何よりも生のCSSゆえのエコシステムの恩恵を受けられるのは大きなメリットです。

styled-components を1年半近く利用したあとに、ほぼすべてのプロジェクトでCSS Modulesを導入していますが、大きな壁にあたったことはありません。とはいえCSS Modulesにもデメリットはあります。最後に、そういった不完全なCSS Modulesと長く付き合っていく方法をまとめておきます。

1. JSからの読み込み順序によって、スタイル適用順序が変わる

この見出しだけでギョッとなった人もいるかもしれません。CSS Modulesはwebpackを用いたバンドル時に、ハードコーディングしたクラス名をランダムな値(または規則性を持ったハッシュ値)に変換し、1つのファイルにUnifyします2

CSSは詳細度が同じセレクタの場合、最後に記述したスタイルが優先される仕様になっているため、このUnify時における各CSSファイルの結合順序が、生成されるCSSファイル内のスタイル順序となります。CSS Modulesは、ES ModulesなどによるJavaScript側からの依存関係を基に結合していくため、JavaScript内のインポート順やCSSファイルのパスの変更だけでスタイルの優先順位が変更されてしまいます。

すなわち、 CSS Modulesにおいてスタイルやオーバーライドの順序は保証されていない ことになります。2つ以上のCSSファイルから1つの要素に対してCSSクラスをあてるとき、適用順序次第でスタイルが変わってしまう場合があるわけですね。そもそもHTMLの仕様として class 属性に与えられたクラス名の順序は適用される順序に何ら影響を及ぼさない3ことになっており、またCSS Modulesの規格として複数のクラスを1つの要素に適用した際の挙動は定義しない4とされています。

対処法

Storybookを用いたビジュアルリグレッションテスト(スナップショットテスト)は必須です。というより、それなりの規模になったらいずれ導入せざるを得なくなるため、早いうちに導入しておきましょう。
コンポーネントを定義しているJavaScriptファイルのリネームなんて誰しもが行う行為であるため、意図しないスタイルの崩れを防ぐためにも、必ず入れておきたいテストです。

費用的な運用コストを抑えたければ、zisuireg-suitを用いて、AWS S3などのオブジェクトストレージ上にスナップショットファイルを置き、CI上でリグレッションを実行させるのが良いでしょう。CircleCIの場合、ヘッドレスブラウザを利用するための circleci/node:xx.xx.xx-browsers Dockerイメージが標準で用意されているので、これを用いると簡単に構築できます。
予算が下りるのであれば、Storybook開発チームがメンテナンスしているChromaticが最も簡単で扱いやすいリグレッションサービスとして挙げられます。リグレッション以外にも、Storybookをオンラインで閲覧でき、またメンバーの権限管理やコメントも投稿可能です。まさにStorybookの有効活用を促進してくれるサービスです。

2. ベンダープレフィックスが自動で付与されない

styled-components の良いところは、CSSクラス名のスコープを限定してくれるだけでなく、ベンダープレフィックスも自動付与してくれるところにあります。

一方でCSS Modulesはそこまで担ってくれないため、自分でベンダープレフィックスを適用するか、別途ツールを導入する必要があります。

対処法

PostCSS
postcss/autoprefixer: Parse CSS and add vendor prefixes to rules by Can I Use

色々考慮した結果、僕はPostCSSを利用してベンダープレフィックスを自動適用させるようにしています。

PostCSSはプラグイン形式でCSSの記法を拡張できるものですが、闇雲にプラグインを追加すると .css ならではのメリットを失うことになります。W3C/WHATWGのCSS草案に記述されているものを先取りする程度に抑えておくことをオススメします。

3. 非標準的なシンタックス

CSS Modulesは import styles from "foo.css" といった、CSSファイルをJavaScript上で読み込む必要があります。これは 明らかに近年重視されているスタンダードな記法への準拠に反した記法が強いられます

styled-components によるスタイリングも一見奇妙な記法に見えますが、JavaScriptの世界へ目線を落とせば単なる文字列に過ぎないため、文法的にはお行儀の良いライブラリと言えるでしょう。

対処法

ありません。スタンダード準拠過激派な人や文化のチームでは、CSS Modulesの採用は避けるべきです。

僕もJade(現Pug)やCoffeeScriptなどを経て、今は「標準最高!」な思想になっていますが、CSS Modulesだけは目をつぶって利用している状態です。

4. クラス名の存在担保

CSS Modulesはwebpack上で対応するクラス名をJavaScriptへ渡すだけであり、 型定義を必要とするTypeScript上では import styles from "./foo.css" がany型(またはそれに近い型)として扱わなければいけません

対処法

.css.d.ts のようなCSS向け型定義をCSSファイルごとに人力で書いていくのは骨が折れるので、typed-css-modulesを利用するのがオススメです。特定のコマンドを叩くだけで、CSSファイルから型定義ファイルを生成してくれます。CSSファイルを監視する watch オプションもありますし、有志によってwebpackプラグインも開発されています。

5. 条件によってクラスを柔軟に変更する

styled-components の場合、生成されたコンポーネントにPropsを与えることができたため、条件をPropsに与えるだけでスタイルを簡単に切り替えられました。

CSS Modulesの場合は、通常のCSSと同様、条件によってクラス名を動的に変更しなければいけません

対処法

classnamesを使いましょう。可変長引数(または配列)で文字列を渡すと Array.prototype.join(" ") のようにvalidな class 属性の値へ変換してくれます。また、クラス名をキー/値を真偽値としたオブジェクトを渡した場合、値が真となるクラス名のみを適用してくれるなど、条件分岐も非常に書きやすいAPIになっています。

import styles from "./styles.css";

const Component = (props) => (
  <div className={classnames(styles.header, { [styles.disabled]: props.isDisabled })} />
);

Custom Propertiesの利用が許されるのであれば、場合によってはCustom Propertiesを style 経由で渡したほうがシンプルに解決できます。

const Component = (props) => <div className={styles.header} style={{ "--color": "red" }} />;

6. margintop などの位置を調整するプロパティは、”親”から渡す

これは技術的な問題というよりもスタイルガイドに近いものですが、CSS Modulesを利用する場合、特定コンポーネントの最上層React Elementには margintop などといった他要素との位置関係を大幅に変えるようなプロパティを適用せず、親コンポーネントからProps経由で渡すようにすると便利です。

普通に書いた場合
// --- child.component.tsx
import styles from "./child.css";
// .child {
//   margin-top: 8px;
//   background-color: blue;
// }

const Child = () => <div className={styles.child} />;

// --- parent.component.tsx
import styles from "./parent.css";
// .parent {
//   background-color: red;
// }

const Parent = () => (
  <div className={styles.parent}>
    <Child />
  </div>
);
位置関係を狂わせるプロパティを親から渡した場合
// --- child.component.tsx
import styles from "./child.css";
// .child {
//   background-color: blue;
// }

type Props = { className?: string };
const Child = (props: Props) => <div className={classnames(styles.child, props.className)} />;

// --- parent.component.tsx
import styles from "./parent.css";
// .parent {
//   background-color: red;
// }
// .content {
//   margin-top: 8px;
// }

const Parent = () => (
  <div className={styles.parent}>
    <Child className={styles.content} />
  </div>
);

そのコンポーネントが本来持つべきクラス名とProps経由で渡されたクラス名の併用は、先程紹介した classnames パッケージを利用すると簡単に併用できます(関数に渡した値が undefinednull の場合は無視されます)。もちろん、親から子に渡す className Propsに対しても classnames は利用できます。

最後に

styled-components のようなCSS in JSのアプローチはとても美しいものです。JSXの登場によって、HTMLという言語をJavaScriptという別言語上で記述する開発手法が一般的になりました。ここにさらにCSSを乗せることによって、すべてをJavaScript上で転がせます。またReactの世界ではスタイリングを含む、すべてをReact Componentで表すことができるようにもなります。
加えて styled-components の開発はとても活発なので、当初ここに掲載する予定だったデメリットのいくつかはバージョンアップによって解決したため、取り下げています。5

とはいえCSS in JSがパラダイムシフトになるようなものでもなく、技術選定の幅を広げただけで、エコシステムの恩恵をすぐに受けられるようになるとも思えないのが正直なところです。Single Page Applicationにおける最適なCSSの運用方法に答えがないのは長年開発者を悩ませる要素の1つであり、どのアプローチをとってもデメリットはつきまといます。上記のデメリットを承知の上で styled-components を利用するのも良し、CSS Modulesのツラサを知った上でCSS Modulesを導入するのも良し、手間を必要とするが安定するBEM的運用も良し、です。プロジェクトやメンバーのリテラシーを考慮した上でゆっくり選定してください。

おひたしおひたし。


  1. JSXはもはやReactのための記法ではなく、PreactVue.jsでも利用できる、1つの汎用シンタックスシュガーとも言えます 

  2. バンドラーにおけるChunks設定やローダ設定によります 

  3. HTML Standard #3.2.6 Global attributes 

  4. css-modules/css-modules - Gitter 

  5. 例えば……Context APIに依存していることから、React Dev Toolsで styled-components が吐き出したコンポーネントをデバッグすると <Context.Consumer> だらけで、デバッグが困難なデメリットがありましたが、v5でHooks APIへ移行されたため解決しています 

jagaapple
こんにちは、よろしくおねがいします。TypeScript/React/Next.js/Swift/Ruby/Ruby on Railsが好きです。 Webデザイン不定期/Webフロント16年/サーバサイド8年/iOS2年
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした