🎀

Reactにおけるスタイリング手法まとめ

2021/06/17に公開
2

開発初期に「どのスタイリング手法でいくべきか」などとよく悩むと思います。
筆者ならそこで「悩むよりもとりあえず書き始めよう」と言いたいところですが、規模が大きいプロジェクトなどでは途中でスタイリング手法を変えようとすると大きな負担になったりと柔軟な変更ができないことがあります。

だがしかし!!

Google先生で調べようものなら
「CSS-in-JSをやめた理由」
「CSS Modulesをやめた話」
などなど...

じゃあ何ならええねん!

そもそもReactにおけるスタイリング手法は多種多様で、時代によって流行が移り変わっていくものでもあり、Googleのトップに来るのは古い情報ばかりです。

そんな悩めるあなたにこの記事では筆者が利用したことのあるスタイリング手法を、CSS-in-JSやCSS Modulesに限らず、筆者の使用感もふまえて紹介していきます。
古い情報だけでなく、zero-runtime CSS-in-JSTailwind CSSなどこれからのデファクトスタンダードになりうる手法も記載してあるので、あなたがスタイリング沼から脱出する手助けになれば幸いです。

なおこの記事ではwebpackなどモジュールバンドラーを利用したフロントエンドフレームワーク(create-react-appなど)を利用している前提で話を進めていきます。

グローバルスタイル

Button.css
.Button {
  padding: 20px;
}
Button.js
import './Button.css';

const Button = () => <div className="Button" />;

普段JSフレームワークを触っていない人からすれば一番直感的な記法です。
ただしスタイルの適用先(スコープ)がグローバルですので、名前の衝突を避けなければなりません。

だからと言って「それならばBEM記法を...」となるのはオススメしません
HTMLとCSSだけだとBEM記法になりますがReactなどのJSフレームワークはその先に連れて行ってくれます。

また、同じスタイルを使いまわしたいという場合でも、基本的にはクラスではなくコンポーネントを使いまわすというのがReact的考えです。(コンポーネント駆動開発)
そもそも一度に複数のスタイルを読み込むのではなく、コンポーネントが読み込まれたときにそのコンポーネントのスタイルだけが読み込まれたほうが、初期レンダリングにかかる時間が少なくUXの向上に繋がります。

使用する場面

App.js_app.jsのようなルートファイルで、グローバルCSSを適用する時(リセットCSSやライブラリ使用時)でしょう。

styleプロップ

<div style={{ height: 10 }}>
  Hello World!
</div>

とても簡単にスタイルを適用できます。
しかしReact公式サイトにも書いてありますが、再レンダリングの度に計算されるのでパフォーマンス的にオススメできません。
また、疑似クラスやメディアクエリにも対応していません。

使用する場面

const Hello = () => {
  const [height, setHeight] = useState(0)
  return (
    <div style={{ height }}>
      Hello World!
    </div>
  )
}

ダイナミックスタイリングとして活用できます。
と、言いたいところですが...

そもそもstyleプロップはパフォーマンスが悪く、ダイナミックスタイリングをしたい場合は読み込むcssを切り替えたり、CSS custom properties (variables)や後述するCSS-in-JSを利用した方が何百倍もいいです。

CSS Modules

Button.module.css
.error {
  color: white;
  background-color: red;
}
Button.js
import styles from './Button.module.css'

const Button = () => (
  <button className={styles.error}>
    Destroy
  </button>
)

CSS Modulesはコンポーネントレベルでスタイルを適用することができ、名前の衝突なども考える必要がありません。
コード分割した上でminifyされた複数のcssファイルへとコンパイルされ、描画のために読み込むCSSを最小限にしてくれるため、パフォーマンスは最高です。

プリプロセッサーであるSassPost CSSとの相性も良く、Post CSSを使用するならコンパイル時に自動でベンダープリフィックスを入れてくれるAutoprefixerのようなものは必須でしょう。
また、後述のCSS-in-JSとは違って生のCSSであるため、開発環境で拡張機能を入れなくても入力補完などしてくれます。

...じゃあなんで全面的に使われていないの?

ファイルが増える事によるDX低下

当たり前ですが、1コンポーネント毎に1ファイルを追加しなければなりません。
昔からCSSを触ってきている人にとっては「何言ってんだこいつ」となると思いますが、3,4行のスタイルを適用したいだけでも1ファイル作成しなくてはいけなく、後述するCSS-in-JSの方がだいぶ書きやすいです

追い風ではない

そもそもCSS Modules自体ががとても古い技術で、CSS Modulesを使用する際に必要なwebpackcss-loaderではdeprecatedとしてメンテナンスステージになるそうです。

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. You can write an own loader based on the current module code.

引用: https://github.com/webpack-contrib/css-loader/issues/1050#issuecomment-592541379

CSS-in-JS

CSS-in-JSと言っても様々なライブラリがあります。
筆者は古のradiumからstyled-jsxTheme UIまで様々なCSS-in-JSに触れましたが、仕組みも異なれば記法も大きく異なってきます。
その中でも有名なstyled-componentsemotion、そしてzero-runtime CSS-in-JSlinariaについて触れていきます。

styled-components (CSS-in-JS)

import styled, { css } from 'styled-components'

const Button = styled.a`
  display: inline-block;
  border-radius: 3px;
  padding: 0.5rem 0;
  margin: 0.5rem 1rem;
  width: 11rem;
  background: transparent;
  color: white;
  border: 2px solid white;

  ${props => props.primary && css`
    background: white;
    color: black;
  `}
`

const Demo = () => (
  <div>
    <Button primary>
      Apply
    </Button>

    <Button>
      Cancel
    </Button>
  </div>
)

上記はstyled-componentsの一般的な記法です。
とても直感的でわかりやすいですね。
ダイナミックスタイリングはもちろん、自動でベンダープリフィックスを挿入してくれたりSassのようなネストセレクターもデフォルトで対応しています。
styled-componentsは検索したらまず出てくる王道のCSS-in-JSです。

だがしかし、みんなが使っているからいいとは限りません。
Googleで出てくるCSS-in-JSをやめた話にはほぼstyled-componentsの愚痴が出てくるでしょう。

パフォーマンス

一度レンダリングしたあとは差分だけレンダリングしてくれるそうですが、jsからスタイルを動的に生成しているので、その一度のレンダリングコストが若干重いです。
普通は問題にはなりませんが、懸念すべき点でしょう。

いちいち命名しなくてはいけない

<Wrapper>
  <Title>
    Hello World!
  </Title>
</Wrapper>

このようにTitleの外から少しスタイリングしたくてもWrapperコンポーネントを作ります。これだけならいいですが複雑になるとInnerやらOuterやらContainerやらStyled〇〇などと命名しなくてはいけません。
また、これはスタイリング用のコンポーネントなのか、それとも機能をもったコンポーネントなのかというのが非常に分かりづらいです。

しかし、これを解決してくれるcss propという機能があります。

<Button css="padding: 0.5em 1em;" />

こちらもstyleプロップと違って複雑な記法が使えたり、styleプロップよりはレンダリングコストが軽いです。

emotion (CSS-in-JS)

import { css, cx } from '@emotion/css'

const Demo = () => {
  const [color, setColor] = useState('white')
  return (
    <div
      className={css`
        padding: 32px;
        background-color: hotpink;
        font-size: 24px;
        border-radius: 4px;
        &:hover {
          color: ${color};
        }
      `}
    >
      Hover to change color.
    </div>
  )
}

emotionにはstyled-componentsのようなcss propstyledもありますが、別で上記のようなフレームワークにとらわれない@emotion/cssがあります。

メリットやデメリットはstyled-componentsとほぼ一緒です。

linaria (CSS-in-JS)

import { styled } from 'linaria/react'

const Button = styled.button`
  background: ${props => props.color};
  padding: 16px 24px;

  &:hover {
    color: ${props => props.color};
    background: white;
  }
`

linariaはzero-runtime CSS-in-JSであり、レンダリング時ではなくビルド時にcssを生成します。
また、propによるダイナミックスタイリングは自動的にCSS custom properties (variables)に変換されます。
よってCSS-in-JSの書きごこちを残したまま、生のcssを読み込んでいるのと同じパフォーマンスを出せます。

しかし、css propはもちろん使用できず、また下記のようなcssタグを使用している場合にはダイナミックスタイリングはできません。

import { css } from '@linaria/core';
import { modularScale, hiDPI } from 'polished';
import fonts from './fonts';

const header = css`
  text-transform: uppercase;
  font-family: ${fonts.heading};
  font-size: ${modularScale(2)};

  ${hiDPI(1.5)} {
    font-size: ${modularScale(2.5)};
  }
`;

const Header = () => <h1 className={header}>Hello world</h1>;

CSS-in-JS全般に言えることですが、CSS Modulesと同じく、描画のために読み込むスタイルは分割されていて最小限であるため、そこのパフォーマンスも期待できます。
また、上記のようにJavaScriptの定数を簡単に使用できたり、polishedライブラリにあるようなヘルパー関数を使えるのがメリットでしょう。

何よりファイルをjsとcssの間で切り替えなくていい...

CSSフレームワーク

過去にはBootstrapのようなCSSフレームワークが流行りましたが、このReactにおいて一つとても相性の良いCSSフレームワークがあるので紹介します。

Tailwind CSS

<div className="md:flex space-x-4 text-white">
  <button className="bg-blue-400">Apply</button>
  <button>Cancel</button>
</div>

ユーティリティファーストのCSSフレームワークで非常にReactと相性がいいです。
CSSフレームワークであるがゆえ、最小限のスタイルを順次読み込んでいくという芸当はできませんが、その代わりPurgeCSSで使用していない不必要なクラスを消しバンドルサイズを最小限にすることができます。

メディアクエリやhoverやfocusなどの状態も、prefixをつけるだけで簡単に指定できます。
また、具体的な数値を指定せずに定数を使用するため、スタイルに一貫性を保てます。
もちろん具体的な数値も使えますし、テーマとしてカスタマイズすることもできます。

また、CSS Modulesにも@applyで組み合わせたり、CSS-in-JSにもtwin.macroというライブラリで組み合わせて使う事も可能で、汎用性が高いです。

他にもTailwind CSSを使ったこんなUIライブラリまで...

何と言ってもその書き心地...

classNameに適用したいクラスを追加していくだけですぐにスタイルが適用できるのでDXは最高です。
もちろん開発環境の拡張機能により入力補完が可能です。

複雑なスタイルはわかりにくい

<figure class="md:flex bg-gray-100 rounded-xl p-8 md:p-0">
  <img class="w-32 h-32 md:w-48 md:h-auto md:rounded-none rounded-full mx-auto" src="/sarah-dayan.jpg" alt="" width="384" height="512">
  <div class="pt-6 md:p-8 text-center md:text-left space-y-4">
    <blockquote>
      <p class="text-lg font-semibold">
        “Tailwind CSS is the only framework that I've seen scale
        on large teams. It’s easy to customize, adapts to any design,
        and the build size is tiny.”
      </p>
    </blockquote>
    <figcaption class="font-medium">
      <div class="text-cyan-600">
        Sarah Dayan
      </div>
      <div class="text-gray-500">
        Staff Engineer, Algolia
      </div>
    </figcaption>
  </div>
</figure>

しかし、メディアクエリや、適用したいスタイルが多くなるとこのように長くなり分かりにくくなります。
そもそも同じスタイルを複数回適用している場合はコンポーネントを分けた方が良い場合もあります。

また、クラス切り替えでは実装できないダイナミックスタイリングは別の方法でやる必要があります。

UIライブラリ

Reactでスタイリングを考えると、Material UIAnt DesignなどのUIライブラリの使用も視野に入ってくるのではないでしょうか。
UIライブラリにはデザインだけでなくModalなどの機能も付属していることが多いので便利ですよね。

しかしUIライブラリ全体の特徴として、学習に時間がかかるという点があります。(癖が強いことが多い)
そこで、カスタマイズがしやすいChakra UIを紹介します。

Chakra UI

<Box p="4">
  <Text
    bgGradient="linear(to-l, #7928CA,#FF0080)"
    bgClip="text"
    fontSize="6xl"
    fontWeight="extrabold"
  >
    Welcome to Chakra UI
  </Text>
</Box>

このようにTailwind CSSのようなユーティリティファーストでプロップから簡単にカスタマイズできます。
Buttonコンポーネントなどのデフォルトのスタイルに癖がないというのも良い点です。
また、Modalなどのコンポーネントもa11yに対応済で機能も充実しています。

まとめ

必ずしも一つに絞らなくてよく、複数の手法を採用するのが現段階では現実的な判断です。
8割はTailwind CSS、ダイナミックスタイルが必要になる2割はCSS-in-JSのように、得意不得意によって柔軟に変えていくことがDXとUXそれぞれの向上に繋がるでしょう。

また、状態によるクラスの切り替えが簡単に書けるclassnamesや、UI開発を補助してくれるStorybookを導入するとさらにスタイリングがやりやすくなります。

この記事を参考に良きスタイリングライフを送ってください!

Discussion

naokinaoki

Emotionの2番目にアクティブな開発者が、ランタイムのCSS生成によるパフォーマンス問題を理由にCSS in JSを捨ててCSS Modulesに移行したらしいです。

ご参考までに。

https://twitter.com/brucel/status/1582787411469533184?s=20&t=pKvPJ5I1dAkSQUIRK2Fs0g

ちぢちぢ

そうなんですか!?最近の事情を追っておらず初耳です、有益な情報ありがとうございます!
記事にその情報を追加させていただきます!