React

React CSSで悩む全ての人へ【2021年版】

React
この記事は約29分で読めます。

こんにちは。株式会社インプルの平澤です。
ReactのCSSで迷われたこと、迷っていることはありませんでしょうか?
そんな方々へReactの各CSSをご紹介致します。

最初に言いますが、どの方法が良いかの正解はありません。「個人的な相性」・「開発チームの相性」・「パフォーマンス」など、どこに重点を置くかでも選択は変わってきます。
それを踏まえた上でご自身のベストプラクティスを見つけてみましょう。

本記事では
・Pure CSS
・CSS Modules
・CSS in JS (styled-component, emotion)
・CSSフレームワーク(Tailwind CSS)
を皆さん大好きReactのトップページに適用して比較し、
各メリット・デメリットをまとめました。


※下記コマンドでプロジェクトを作成した前提で進んでいきます。

$ npx create-react-app {プロジェクト名} --template typescript

Pure CSS

Pure CSSとはcreate-react-appした時にデフォルトで適用されているCSSです。
プログラミング初学者の多くの方はHTML/CSSから学習されると思いますが、それに近しいスタイルの当て方です。

学習コストが低いですが、正直あまりおすすめはしません。
スコープが広いのでクラス名が衝突しやすく、それを避けようと命名が冗長的になっていき結果として管理が難しくなりがちです。
Pure CSSに近しい形というのであればCSS Modulesを強く推します。


Pure CSS でReactのトップページ

//App.tsx

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
//App.css

.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

create-react-appのDefaultのままです。

メリット・デメリット

メリット

  • 学習コストが低い

デメリット

  • スコープが広いのでクラス名での衝突が起きやすい。

CSS Modules

CSS Modulesは、CSSの適用範囲をコンポーネント単位で閉じようという考えで作られたものです。
Reactはコンポーネント指向であり、CSS ModulesのチームメンバーにReactユーザーが多いことから「React + CSS Module」の相性が良いことは言うまでもありません。
Pure CSSに書き方が近しいことから学習コストが低く、万人向きです。

こちらがCSS Modulesがチームメンバーの書いた記事です。
https://glenmaddern.com/articles/css-modules

CSS Modulesを使用する上で1つだけ念頭に置いておきたいのは、「将来非推奨になる可能性がある」ということです。
ここでは詳しくは説明しませんが、create-react-appで構築される中身を見るとcss-loaderがあります。そのissueで「 In the near future we want to deprecate CSS modules」といった書き込みがありました。
https://github.com/webpack-contrib/css-loader/issues/1050
これが原因でCSS ModulesからCSS in JSに移行した事例も多かったようです。


CSS ModulesでReactのトップページ

//App.tsx

import React from 'react';
import logo from './logo.svg';
import AppModule from "./App.module.css"; 

function App() {
  return (
    <div className={AppModule.App}>
      <header className={AppModule["App-header"]}>
        <img src={logo} className={AppModule["App-logo"]} alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className={AppModule["App-link"]}
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

1, App.css App.module.css へ変更します。

2, App.tsx で変更したcssファイルを任意のmodule名でimportする

3, 各classNameを{module名.class名}に変更する

※class名にハイフンがある場合に{module名.class名}は使えないので、{module名[“class名”]}にします。

App.module.css は Pure CSSで記載した App.css と同じコードなので省略します。

記述がPure CSSとあまり相違ないので非常に親しみやすい印象を感じます。

メリット・デメリット

メリット

  • Pure CSSに近く、学習コストが低い
  • コンポーネント単位でscopeを閉じれるのでclass名の衝突が起きない
  • create-react-appで導入されているので、すぐに使える

デメリット

  • 将来非推奨になる可能性がある
  • CSS in JSと比較すると「css・js」と2つのファイルに分かれることは避けられない

CSS in JS

CSS in JSはCSSをJSファイルの中で記述できるものです。
PureCSS, CSS Modulesに比べるとファイルが一つで済みますし、propsによる動的にスタイルを変更することもできるなどのメリットがあります。
しかし、若干ですがレンダリング速度がCSS in JSを使うと遅くなるなどのデメリットもあります。jsからスタイルを動的に生成するのでその分レンダリングが遅れる。といったイメージです。

ただ、阿部寛さんのホームページ並の爆速表示を求めていないのであればそこまで気にするデメリットでもありません。
http://abehiroshi.la.coocan.jp/

CSS in JSの代表格としてstyled-components・emotionの2つがあります。
出た順番として、 styled-components > emotion の順番です。
emotionの方が機能は多いです。が、インターネットの記事の量でいくとやはり先発のstyled-componentsが多いです。
それも踏まえた上で2つの違いを見ていきましょう。

各ライブラリのGit hubとnpm trend

まずnpm trendでそれぞれを比較してみます。
2021/12/6時点でいくと emotion > styled-components といった感じでしょうか。
styled-componentsは2016年から今まで一定の右肩上がりですが、emotionは2018年8月に出てから急激に伸びて1年後にはstyled-componentsを抜いてます。
あくまでnpmでの指数での話なので参考までに。

Git hubのスター数で比較するとstyled-componentsの方が倍以上になっています。

styled-components (スター35.3k)
https://github.com/styled-components/styled-components

emotion (スター14.1k)
https://github.com/emotion-js/emotion

styled-components

CSS in JSにおいてstar数は圧倒的なstyled-componentsです。

オープンソースのプロジェクトを見るとやはりCSS in JSの中ではstyled-componentsが多いイメージで、実際にcodeSandBoxではReact + Next + styled-componentで書かれてるみたいです。
styled-componentsにおいてディレクトリ構成は一つの悩みの種になると思いますが、オープンソースのプロジェクトを参考にするといいかもしれません。
https://github.com/codesandbox/codesandbox-client

当ライブラリのデメリットとして、パスカルケースのJSXタグを見たときに、styled-componentsでスタイルを当てたコンポーネントなのか、機能を持たせたコンポーネントなのかが分かりにくいです。
このデメリット解消のためにはプロジェクト内で明確なルールを設ける必要があるように感じます。


styled-componentsでReactのトップページ

まず最初に下記コマンドでstyled-componentsのパッケージをインストールします。

$ yarn add styled-components
$ yarn add @types/styled-components

@types/styled-components はtypeScriptを使用している際に必要です。

//App.tsx

import React from 'react';
import logo from './logo.svg';
import styled, { keyframes }  from 'styled-components';

function App() {
  return (
    <Container>
      <Header color="white">
        <Logo src={logo} alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <AppLink
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
          color = "#61dafb"
        >
          Learn React
        </AppLink>
      </Header>
    </Container>
  );
}

const AppLogoSpin = keyframes`
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
`

const Container = styled.div`
  text-align: center;
`

const Header = styled.header`
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: ${(props) => props.color}
`

const Logo = styled.img`
  height: 40vmin;
  pointer-events: none;
  animation: ${AppLogoSpin} infinite 20s linear;
`

const AppLink = styled.a`
  color: #61dafb;
`

export default App;

1, styled-componentsから任意の名前でModuleをimportします。(命名はなんでも大丈夫ですが、styledという命名が多く見受けられます)

2, 任意の名前でcssを当てたコンポーネントを作成します。
形としては、

const コンポーネント名 = Module名.要素`
 適用するcss
`

といった形になります。例としてコードを参照ください

3, 作成したコンポーネントをrender内で呼び出すとstyled-componentsでcssを当てたコンポーネントを描画できます

※keyframesはReactのTopページのアニメーションで使用されており、styled-componentで使用する場合にはimportする必要があります。

propsの適用の例としてHeaderコンポーネントでcolorを渡して、cssに適用する記述をしました。

メリット・デメリット

メリット

  • スコープを絞ることができる。
  • propsで動的にスタイルの変更ができる
  • Jsファイルに全て記述できるので、可読性に優れている

デメリット

  • styled-componentsによってスタイルを当てたコンポーネントか、機能を持たせたコンポーネントかの区別がつきづらい
  • テンプレートリテラルでの記述なので、エディタによってはシンタックスハイライトを付けれない

emotion

emotionも非常に人気のCSS in JSのライブラリで、多機能です。
この記事では@emotion/reactを使用しますが、@emotion/styledを使用するとstyled-componentsと同じ書き方で実装できます。

@emotion/reactは私は今一番に推しているライブラリです。
まず当記事の冒頭で述べましたが、CSSの記述は相性があります。自身は普段React Nativeを書く時間が多く、@emotion/reactは「React NativeのStyleSheetを使った記述に近い」「propsによる動的にstyleを変える」「object styleで記述できる」「より少ない記述で使える」といった要望に一番近かったです。

ただ、デメリットもあり、構築に手間がかかります。
全てのファイルにJSX Pragmaを付けなければいけないというデメリットがあり、JSX Pragmaをつけたくなければcreate-react-appでの設定を上書きする必要があります。
が、ここで設定も1から説明しますのでご心配なく。


emotionでReactのトップページ

まず最初に下記コマンドでemotion(@emotion/react)のパッケージをインストールします。

$ yarn add @emotion/react @emotion/babel-plugin

上記のパッケージのみをインストールした状態でコードを記述すると下記のようになります

//App.tsx

/** @jsxImportSource @emotion/react */
import React from 'react';
import logo from './logo.svg';
import { css , keyframes }  from '@emotion/react'

function App() {
  return (
    <div css={styles}>
      <div css={header} >
        <img css={logoStyle} src={logo} alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          css={linkStyle}
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </div>
    </div>
  );
}

const AppLogoSpin = keyframes`
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
`

const styles = css({  
  textAlign: "center"
})


const header = css({
  backgroundColor: "#282c34",
  minHeight: "100vh",
  display: "flex",
  flexDirection: "column",
  alignItems: "center",
  justifyContent: "center",
  fontSize: "calc(10px + 2vmin)",
  color: "white"
})

const logoStyle = css({
  height: "40vmin",
  pointerEvents: "none",
  animation: `${AppLogoSpin} infinite 20s linear`
})


const linkStyle = ({
  color: "#61dafb"
})


export default App;

1, JSX Pragmaをファイル内に記述する

/** @jsxImportSource @emotion/react */

2, @emotion/react から { css }をimportする

3, 各スタイルを当てたい要素にcssを追加する

4, スタイルを当てる。(ここではObject styleで記述しましたが、styled-componentsのようにテンプレートリテラルでも可能です)

以上がemotionでの記述になりますが、全てのファイルにJSX Pragmaを記述するのがめんどくさいという方のために省略できる環境構築は下記にまとめました。

JSX Pragmaを省略する設定

まずCRACOをインストールします。
CRACOはCreate React App Configuration Overrideの略で、読んで時の如くcreate-react-appの設定を上書きできるライブラリです。

$ yarn add @craco/craco

次にプロジェクト直下に craco.config.js を作成します。
内容に関しては下記のコードをそのままコピペで問題ありません。

module.exports = {
  babel: {
    presets: [
      [
        "@babel/preset-react",
        { runtime: "automatic", importSource: "@emotion/react" },
      ],
    ],
    plugins: ["@emotion/babel-plugin"],
  },
};

次に、pakage.jsonのscripts内を、cracoを通して起動・ビルドするよう変更します。
下記左のコードにデフォルトでなってるので、丸っと右側のコードに変更します

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
  },
"scripts":{
 "start": "craco start",
 "build": "craco build",
 "test": "craco test",
},

最後にtsconfig.jsonに下記の “jsxImportSource”: “@emotion/react” を追記します。

"compilerOptions": {
  ...
  "jsxImportSource": "@emotion/react"
},

以上です!ではこれでコードを書いてみましょう。

import React from 'react'
import logo from './logo.svg'
import { keyframes } from '@emotion/react'

function App() {
  return (
    <div css={styles.container}>
      <div css={styles.header}>
        <img css={styles.logoStyle} src={logo} alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          css={styles.linkStyle}
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </div>
    </div>
  )
}

const AppLogoSpin = keyframes`
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
`

const styles = {
  container: {
    textAlign: 'center',
  },
  header: {
    display: 'flex',
    alignItems: 'center',
    flexDirection: 'column',
    backgroundColor: '#282c34',
    minHeight: '100vh',
    justifyContent: 'center',
    fontSize: 'calc(10px + 2vmin)',
    color: 'white',
  },
  logoStyle: {
    height: '40vmin',
    PointerEvents: 'none',
    animation: `${AppLogoSpin} infinite 20s linear`,
  },
  linkStyle: {
    color: '#61dafb',
  },
}

export default App

JSX Pragmaは削除して大丈夫です。

あえて上記のemotionのコードとは違った書き方をしてみました。
React nativeのStyleSheetと似たような書き方です。

keyframesを使用してるのでModuleをimportしてますが、使用しないのであればもはや@emotion/reactから何もimportする必要すらありません。
なぜimportする必要すらないのは記事の最後にまとめました。

※type scriptのバージョンによっては「CSSObjectに{textAlign : string} の型が合わない。」といったエラーが起こることがありますが、下記のように記述すればエラー解消できます

    textAlign: 'center' as const,
メリット・デメリット

メリット

  • styled-componentに比べ記述が少なくて済む
  • テンプレートリテラルやObject styleなど、記述の幅が広い

デメリット

  • 最適化するための環境構築に手間がかかる

CSSフレームワーク

Tailwind CSS

TailwindはUtility First な CSS フレームワークとして最近よく耳にするようになりました。

classNameに適用したいクラスを追加していき、スタイルを適用する形です。
慣れてしまえば非常にスピーディな開発ができるはずです。
メディアクエリやhoverやfocusなどの適用も可能です。
CSS in JSのライブラリと組み合わせることもできますし、活用法が幅広い印象です。

が、CSS in JSのemotion同様にcreate-react-appの設定を上書きする必要があります。
また、スタイルの当て方がTailwind独自のものなので他に比べて学習コストが高いことや、エディタによってはシンタックスハイライトを付けれないことがあります。
(VSCodeであれば Tailwind CSS IntelliSenseを入れると入力候補やシンタックスも効いてAwesomeです)

TailwindでReactのトップページ

https://tailwindcss.com/docs/guides/create-react-app
上記がtailwindの公式ドキュメントです。必要に応じてご参照ください。
公式ドキュメント通りに構築すれば何も問題ないはずですが、下記にも手順をまとめました。(長いです)

まず最初に下記コマンドでtailwindのパッケージと、必要なプラグインをインストールします。
ドキュメントにも書いてますが、2021/12/6現在ではPostCSS8はサポートしてないので7を指定してインストールする必要があります。

$ yarn add tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

次にお馴染みのCRACOをインストールしてpackage.jsonを修正します。

$ yarn add @craco/craco
  {
    // ...
    "scripts": {
     "start": "craco start",
     "build": "craco build",
     "test": "craco test",
     "eject": "react-scripts eject"
    },
  }

次にcraco.config.js にプラグインを追加します。

// craco.config.js
module.exports = {
  style: {
    postcss: {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
      ],
    },
  },
}


次にtailwindの構成ファイルを作成します。

$ npx tailwindcss-cli@latest init

上記コマンドで、tailwind.config.js が自動で作成されるので、purgeのところに使用するファイルの拡張子を指定します。
Reactのプロジェクトは基本的に下記をコピペで問題ありません。

  // tailwind.config.js

  module.exports = {
   purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
  ...
  }

次にcreate-react-app時に生成されている src/index.css へ下記をコピペし、src/index.tsx でcssファイルをimportします。
最上位コンポーネントである index.tsx でimportすることで全てのコンポーネントでtailwindを適用できるようになります。
importはcreate-react-app v4系のtemplate type scriptであれば最初からimportされているはずです。

/* ./src/index.css */

@tailwind base;
@tailwind components;
@tailwind utilities;
 // src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

...

以上でtailwindの環境構築終了です!

//App.tsx

import React from 'react';
import logo from './logo.svg';

function App() {
  return (
    <div className={styles.container}>
      <div className={styles.header} >
        <img className={styles.logoStyle} src={logo} alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className={styles.linkStyle}
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </div>
    </div>
  );
}


const styles = {
  container:`
    text-center	
  `,
  header:`
    flex
    items-center
    flex-col
    min-h-screen
    justify-center
    text-3xl
    text-white
    bg-gray-800
  `,
  logoStyle: `
    h-72 
    pointer-events-none	
    animate-spin-slow
  `,
  linkStyle :` 
    text-blue-400
  `
}

export default App;

tailwindをreactで使う場合にはclassNameの中にclassを書いていく場合が多いです。
私がrender内がごちゃっとするのが嫌なのでObjectを外に作ってそこに記述しています。

また生のcssや、CSS in JSでスタイルを書くのとは違い、既に用意されているクラスを当てていく形です。
クラスに関してはtailwindのドキュメント内で検索すると出てきます。
(わざわざ検索しなくても省略して書けば何となくいけたりもします。)
Documentation - Tailwind CSS
Documentation for the Tailwind CSS framework.

classが用意されていないスタイル当てたい場合には、tailwind.config.js内に記述して自分で拡張していく形になります。

※左のコードはReactのトップページを完全再現はできていません。
完全再現するためにはcolorやfontSizeなどをtailwind.config.jsに記述する必要があります。

アニメーションは既に用意されているクラスがありますが、用意されているスピンアニメーションが「1秒で1回転」なので、reactのトップページで使われている「20秒でロゴが1回転」などの実装はtailwind.config.js内に記述してあげる必要があります。

//tailwind.config.js

 ...
  theme: {
    extend: {
      animation: {
        'spin-slow': 'spin 20s linear infinite',
     }
    },
  },
  ...

下記がtailwindでanimationを実装する場合のドキュメントです。
https://tailwindcss.com/docs/animation


メリット・デメリット

メリット

  • クラスが既に用意されており、記述量が少なくて済むため実装が早

デメリット

  • tailwindのクラスを覚える or 調べるコストがかかる
  • スタイルの細かい調整には不向き

補足

JSX Pragmaとは

emotionの時に出てきたJSX Pragmaとは何でしょうか。
https://emotion.sh/docs/css-prop#jsx-pragma
emotionの公式ドキュメントを見るとざっくりですが説明されてます。

Pragmaについて一言でまとめると、コンパイラを操作できるものといったイメージです。
create-react-appで構築したReactプロジェクトにおけるコンパイラとは何でしょうか。
そうですbabelです。

では

/** @jsxImportSource @emotion/react */

これの役目を見ていきます。
Reactの要素はコンパイラによって下記のような関数に変換されてます。

React.createElement("要素")



emotionでコードを記述した時に

<div css={hoge}> ... </div>

と書きましたが、creact-react-appで構築されたbabelで変換される関数 React.createElement("要素") ではcss propsが読み込めません。
そこで Pragmaによって jsx() 関数に変換してcss propsが適用されるようになっています。
下記emotionソースコードの jsx.js => emoiton-element.js を見ると詳細なコードが見れます。
https://github.com/emotion-js/emotion/tree/main/packages/react/src

それをcreate-react-appの設定を上書きし、常にemotionのjsx()を通してコンパイルされることによりimportすらすることなく css props を適用できるようになったわけです。

まとめ

いかがでしたでしょうか。

冒頭でも述べましたが、「個人的な相性」・「開発チームの相性」・「パフォーマンス」何を優先するかで最適解は変わってくると思います。
ただそんな中で私が一番コーディングしやすく、可読性の観点でよく感じたのでemotionです。

CSS in JSに関しては各ライブラリのほんの一部しか紹介できていませんので、一度ご自身でドキュメントを確認するより良いReactライフを送れると思います。