ReactのCSSの選択肢を比較してみた
世はReact with CSS 戦国時代...!
- Pure CSS
- CSS Modules
- Styled Components(CSS in JS)
- Emotion(CSS in JS)
- Linaria(CSS in JS)
- Tailwind
- CSS Framework(Chakura UIなど)
と、Reactと共に使うCSSの選択肢は非常に多岐に渡り、学習者を混乱に陥れることもしばしばかと思います。
私が実装経験がない
- Tailwind
- CSS Framework
については今回語りませんが、それ以外のCSS実装方法を全く同じコンポーネントを実装することで比較してみました。
下記が実際にコーディングしたGitHubになります。
Pure CSS
通常のCSSです。
CSSは全てがグローバルで管理されてしまうので保守・管理にコストがかかり、崩壊しやすいです。
なのでBEMなどのCSS設計でカバーして運用していくケースが多いのではないでしょうか?
BEMは命名でスコープを作るという考え方です。
下記が実際BEMでコーディングしたコンポーネントです。
import React, { FC } from 'react'
export const BemComp = () => (
<section className="charaSection">
<ul className="charaSection__list">
<Card charaName="kirby" />
<Card charaName="waddleDee" />
<Card charaName="dedede" />
<Card charaName="metaKnight" />
</ul>
</section> )
type CardProps = {
charaName: 'kirby' | 'waddleDee' | 'metaKnight' | 'dedede'
}
const Card: FC<CardProps> = ({ charaName }) => (
<li className="charaSection__list__item">
<a href="" className={`charaSection__list__item__inner--${charaName}`}>
<span className="charaSection__list__item__inner__name">{charaName}</span>
</a> </li>
)
こちらがCSS側です。
.charaSection__list__item:last-of-type {
margin-right: 0;
}
[class^="charaSection__list__item__inner--"] {
position: absolute;
inset: 0;
display: block;
width: 100%;
border-radius: 10px;
box-shadow: 5px 5px 0 0 rgb(0 0 0 / 15%);
}
.charaSection__list__item__inner--kirby {
background: violet;
}
.charaSection__list__item__inner--waddleDee {
background: yellow;
}
スコープが広い分命名が長くなるのは当然なのですが、やはり冗長に感じられます。
StylusやSassなどのCSSプリプロセッサでこの記述を簡略化してみましょう。
.charaSection
padding: 30px;
background: pink;
&__list
display: flex;
justify-content: center;
margin: 0 auto;
max-width: 1000px;
&__item
...
スッキリしましたね!
ですが、こちらのコードだと
- grepできず該当のコードの検索がかけられない
- ネストが深いとコードを追うのが非常に困難になる
と、保守が非常に大変になってしまいます。
CSS Modules(Css Loader)
Webpackのローダーにより、CSSのクラスをJSで読み込むという戦略です。
Web ComponentsによるCSS Moduleとは別物なので注意してください。
(筆者はいずれはWeb ComponentsによるCSS ModuleによってCSS戦国時代が終焉すると予想しています。今回詳しく解説しませんが、気になる方はぜひ調べてみてください。)
話が少しそれましたが、Css LoaderによってCSSにScopeという概念を生むことができます。
自動で CSSファイル名__CSS内クラス名__ハッシュ値
というクラスが付きます。
ただし、後述するように現在メンテナンスオンリーとなっています。
.inner--metaKnight {
background: purple;
position: absolute;
inset: 0;
display: block;
width: 100%;
border-radius: 10px;
box-shadow: 5px 5px 0 0 rgb(0 0 0 / 15%);
}
.inner--dedede {
background: rgb(104, 104, 248);
position: absolute;
inset: 0;
display: block;
width: 100%;
border-radius: 10px;
box-shadow: 5px 5px 0 0 rgb(0 0 0 / 15%);
} .name {
color: #000;
}
このようにスコープが狭くなったことでクラス名が短くなり、可読性が上がりました。
import React, { FC } from 'react'
const styles = require('./comp.module.css') //ts
export const CssModuleComp = () => (
<section className={styles.charaSection}>
<ul className={styles.list}>
<Card charaName="kirby" />
<Card charaName="waddleDee" />
<Card charaName="dedede" />
<Card charaName="metaKnight" />
</ul>
</section>
)
type CardProps = {
charaName: 'kirby' | 'waddleDee' | 'metaKnight' | 'dedede'
}
const Card: FC<CardProps> = ({ charaName }) => (
<li className={styles.item}>
<a href="/" className={styles[`inner--${charaName}`]}>
<span className={styles.name}>{charaName}</span>
</a> </li >
)
上記のサンプルではTS対策のためCommonJS記法を用いていますが、Webpackでひと手間加えると、CSS Modulesも型安全に利用することができます。
【補助資料】
実際に実行されたdevtoolを見ると、クラス名が一意となるように命名されているのがわかります。
また、とあるissueに対して以下のようなcontributorの回答がありました。
Sorry, it is out of scope CSS modules spec, In the near future we want to deprecate CSS modules, there are a lot of other solutions - BEM, CSS-in-JS, CSS-in-JS without runtime https://github.com/callstack/linaria, future CSS module (WICG/webcomponents#759), do not confuse with current CSS modules, Web Components and shadow DOM.
CSS modules is maintenance stage (only fixes), it is really old technology and very controversial. We will support them for a while so that all developers can migrate, but no new features, sorry.
ごめんなさい。これはCSSモジュールの対応外です。将来私たちはCSS modulesをdeprecatedにしたいと思っています。他にもたくさんのソリューションがあります。 - BEM,CSS-in-JS,ゼロランタイムのCSS-in-JSであるLinaria、将来的にはWebComponentsのCSS module(これは私たちのCSS modulesではないことに注意してください。)CSS Modulesは現在メンテナンスステージ(修正のみ)にあります。CSS Modulesはとても古い技術で物議を醸しているものです。私たちは開発者がマイグレートするまでの間のサポートをしますが、新しい技術の追加は行いません。
https://github.com/webpack-contrib/css-loader/issues/1050?
こちらを見ると、今から新規採用するのは少しリスキーかな...と思っています。
CSS Modulesについてのまとめ
メリットとしては
- scopeがついて運用が安全で楽になる
- Pure CSSに近い記述でかける
- 既存のLintがそのまま使える
デメリットとしては
- コンポーネントを跨ぐCSS共通化が難しい
- ゆくゆくはdeprecatedになりそう
となります。
また、前述のCSSを見ると、.inner--metaKnight
と .inner--dedede
重複する記述が出てきていることからわかる通り、シングルクラスでの運用はあまり適していないという印象を持ちました。
CSS in JS (Styled Components, Emotion)
CSSをJSで記述してしまいましょう!という発想です。
記述ファイルが一つで済むので非常に楽で、JS故にpropsで動的に一部のスタイルを変更することができます。
また、当然JSが読み込まれないと描画されません。パースの問題でパフォーマンスに影響を与えるという報告も見られます。
CSS同様スコープを作ることが可能です。
代表格としてStyled ComponentsとEmotionの二つがあるので、それぞれの違いを見ていきましょう。
Styled Compoennts
タグ付きテンプレート文字列による記述をサポートしています。
また、タグ名とセットで記述するのが特徴です。
Emotion
タグ付きテンプレート文字列/オブジェクト記法の二つがサポートされています。
Styled Componentsより後発ということもあり、痒いところに手が届いている印象です。
また、タグ名と切り離してスタイルを宣言できます。
Emotion v11になってからはTSをサポートした都合で、jsxの型を上書きしなくてはなりません。
こちらの設定がちょっと手間であり、また副作用としてFragmentを省略記法で書くことができなくなりました。
Styled Components
実際に書いてみた例が以下です。
import React, { FC } from 'react'
import styled from 'styled-components'
export const StyledComp = () => (
<CharaSection>
<List>
<Card charaName="kirby" color="violet" />
<Card charaName="waddleDee" color="yellow" />
<Card charaName="dedede" color="rgb(104, 104, 248)" />
<Card charaName="metaKnight" color="purple" />
</List>
</CharaSection>
)
type CardProps = {
charaName: 'kirby' | 'waddleDee' | 'metaKnight' | 'dedede'
color: string
}
const Card: FC<CardProps> = ({ charaName, color }) => (
<Item>
<Inner color={color} href="/">
<Name>{charaName}</Name>
</Inner>
</Item >
)
実際にスタイルを宣言しているものの一部が以下になります。
const Inner = styled.a`
position: absolute;
inset: 0;
display: block;
width: 100%;
border-radius: 10px;
box-shadow: 5px 5px 0 0 rgb(0 0 0 / 15%);
background: ${({ color }) => color}
`
const Name = styled.span`
color: #000;
`
color
でpropsを渡しているのが確認できますね。
styled.a
は、aタグのstyled componentsを宣言するという意味です。
Styled Componentsまとめ
メリット
- propsとしてデータを受け取れるので、動的にスタイルの変更が可能です。
- スコープを作ることができます。
デメリット
- タグとCSSの記述がセットになっているのは賛否両論ありの部分です。
- 通常のpropsとスタイルとしてのpropsが混在してしまいます。
- ぱっと見でなんのタグか判別できないため、属性の設定のミスが増えやすいです。
- 通常のタグとStyled Componentsの区別もつきにくいです。
また、従来のLintやエディタ補完、シンタックスハイライトに対する懸念をあげれれている方もいますが、CSS-in-JSが浸透してきたこともあり、現在のエディタの拡張やTypeScriptでかなりカバーできると私は感じています。(なんならTSで予測補完がついて超便利です。)
Emotion
/** @jsxRuntime classic */
/** @jsx jsx */
import React, { FC } from 'react'
import { css, jsx } from '@emotion/react'
export const EmotionComp = () => (
<section css={charaSectionCss}>
<ul css={listCss}>
<Card charaName="kirby" color="violet" />
<Card charaName="waddleDee" color="yellow" />
<Card charaName="dedede" color="rgb(104, 104, 248)" />
<Card charaName="metaKnight" color="purple" />
</ul>
</section>
)
type CardProps = {
charaName: 'kirby' | 'waddleDee' | 'metaKnight' | 'dedede'
color: string
}
const Card: FC<CardProps> = ({ charaName, color }) => (
<li css={itemCss}>
<a href="/" css={innerCss(color)}>
<span css={nameCss}>{charaName}</span>
</a> </li >
)
前述の通り、jsx
の型を上書きするためにemmetをつけています。
Webpack側で設定することで一括でこちらを設定することも可能です。
別記事で以前こちらの設定を行いました。
動的に記述したい場合は関数を噛ませて、引数として渡してあげれば良いです。
const innerCss = (color: string) => css`
position: absolute;
inset: 0;
display: block;
width: 100%;
border-radius: 10px;
box-shadow: 5px 5px 0 0 rgb(0 0 0 / 15%);
background: ${color};
`
const nameCss = css`
color: #000;
`
Emotionまとめ
Styled componentsと比較してタグとスタイルを分離できます。
また、記述方法もオブジェクト or テンプレート文字列と柔軟です。
ただし、jsxにcssPropsを生やしている都合上、環境設定周りに少しクセがあります。
筆者は新規案件に関してはEmotionで進めることが多いです。
CSS in JSパフォマンス問題
ここでCSS in JSの大きな問題点について言及しておきます。
クライアント側でJSがCSSとして解析されるので、何度も走るような部分で記述してしまうと一気にパフォーマンスを落とします。
また、そうしたミスを犯さなくてもクライアント側に負荷がかかってしまっています。
下記のサンプルではCSSライブラリのパフォーマンスを比較することができます。
このように、ビルド後は生のCSSとして吐き出される css-modulesに対してパフォーマンスに差があることがわかります。
Linaria
そんな中彗星のように現れたCSS in JSがLinariaです。
zero runtimeが売りであり、他のCSS in JSと違いビルド時にHTMLのLinkタグとして抽出されるので、クライアント側で負荷がかかりません。
また、書き方もstyled-componentsとほとんど同じなので、他のCSS in JSが扱えればほとんど学習コストはかかりません。
内部的にcss variablesやcssカスタムプロパティが使われているので、当然IE11はNGです。
CSS記法とstyled記法があり、styled記法のみ動的な書き方が可能です。
import React, { FC } from 'react'
import { styled } from '@linaria/react'
import { css } from '@linaria/core'
export const EmotionComp = () => (
<section className={charaSectionCss}>
<ul className={listCss}>
<Card charaName="kirby" color="violet" />
<Card charaName="waddleDee" color="yellow" />
<Card charaName="dedede" color="rgb(104, 104, 248)" />
<Card charaName="metaKnight" color="purple" />
</ul>
</section>
)
type CardProps = {
charaName: 'kirby' | 'waddleDee' | 'metaKnight' | 'dedede'
color: string
}
const Card: FC<CardProps> = ({ charaName, color }) => (
<li className={itemCss}>
<Inner href="/" color={color}>
<span className={nameCss}>{charaName}</span>
</Inner>
</li >
)
build後はCSSとなるため、上記のようにclassName
にlinariaを渡していきます。
動的に描きたい部分(上記であればInner
)はCSS記法が書けないため、Styled Components同様タグ名を隠蔽してしまいます。
実際にLinariaを使い、どれぐらいパフォーマンス面が改善されたかの比較記事があります。
こちらも合わせてご確認ください。
Linariaまとめ
- パフォーマンス面での懸念が解消される。
- トレードオフとしてCSS記法でのダイナミックな記述ができなくなっている。
- こちらは公式でいくつか解決案が挙げられています。(CSS custom propertiesなど)
https://github.com/callstack/linaria/blob/master/docs/DYNAMIC_STYLES.md
- こちらは公式でいくつか解決案が挙げられています。(CSS custom propertiesなど)
import React from 'react';
import { css } from '@linaria/core';
const box = css`
height: var(--box-size);
width: var(--box-size);
`;
export function Box({ size }) {
return (
<div
className={box}
style={{ '--box-size': size }}
/> );
}
最初からIEを切っている前提なので全然アリかと思います。
まとめ
Web ComponentsによるCSS Modulesの到来までは、ReactのCSS周りはLinariaで記述するのが良いと感じました。
しかしまだまだバージョンの浅いライブラリなので、小さなプロジェクトから導入し、様子をみたいと思います。
また、IE11が非対応なので注意です。Styled ComponentsやEmotionでもgridを扱う場合はひと手間必要です。
Discussion
TwitterにてLinariaもcss modulesの仕組みに依存している旨をご指摘いただいております。
後日こちら調査しリライト予定です。
Linaria初めて知りました。ありがとうございます。
Linariaはstyled-componentsのような書き方もできるようで、その場合はpropsからオプションを渡すことができるようです。
css-modulesのデメリットとして
のプロパティの重複を例に出されて、シングルクラスでの運用に適していないと書かれていますが、scssなどのmixinを利用すると
みたいに共通部分を抽出して抽象化できますし、
・コンポーネントを跨ぐCSS共通化が難しい
についても、css modulesやmixinの共通ファイルを各コンポーネントで読み込めばいいだけなのでは?と思いました‥
CSS-in-JSとPure CSSの比較が、React開発においてスタイル管理の選択肢を明確に示している点が勉強になりました。最近、私もAPIの開発に携わっており、UI要素のテストに多くの時間を費やしているのですが、効率化のために「EchoAPI」というオフラインで使えるAPIテストツールを活用しています。VS Codeとシームレスに統合されていて、他のツールを切り替える必要がなく、スタイルとAPIテストを一緒に管理できるのが便利なんです。