🥨

今Reactを使うならピュアなCSSをサポートするUIフレームワークやライブラリを選定するのがマルそうと思った話

2024/11/08に公開
2

最近個人開発でNext.jsの環境を立ち上げた際に、スタイリングをどうやってやろうかなと迷ったので初心に帰って最近のスタイリングについて調べ直してみました。結構面白かったので、記事にまとめることにしたので、よかったら読んでみてください。

本記事では以下に触れます。

  • React界隈におけるスタイリング方法の歴史的変遷
  • なぜタイトル通りピュアなCSSをサポートする機構が良いと感じたか
  • おすすめのスタイリング機構

ざっと振り返るReactのスタイリング方法の歴史的変遷

私が最終的にピュアCSSを推したいと思った理由をお話する前に、爆速で変わり続けるReactとともに、スタイリングはどう変遷していったのかをざっとまとめたいと思います。

(こういうのって、歴史と組み合わせて理解すると、結構深まりますよねと思っているのは私だけでしょうか…、いえそんなことはないはず😊)

大昔: CSS Modules vs CSS-in-JS

私はなんだかんだで、全然初心者だった段階を含めて3年くらいReactと関わりを持っていますが、その3〜5年前の界隈ではCSS Modules vs CSS-in-JSが大流行りしていた気がしています。

CSS Modulesは、Webpackなどのバンドルツールと合わせて利用することにより、CSSにスコープを作ってグローバルな衝突を避けることができる仕組みです。当時からその名の通りカスケードしまくるCSSには嫌気がさしていたということや、Scssと一緒に利用できること、JavaScriptファイルにインポートしてサクッと利用できることから、好んで使っていた記憶があります。一方でCSS Modulesはあくまでスタイルシートとして定義するため、JavaScriptの動的な変更を伝搬させるのが少々難しいということがありました。

このようなツラミを解決してくれていたのが、CSS-in-JSという仕組みです。JavaScriptの中に動的にCSSを書き込むことができ、上記のCSS Modulesのツラミを解決してくれました。JavaScriptの中にスタイルも埋めるような手法というのは当初やや抵抗がありました(JavaScript=ロジック、CSS=スタイリングで責務が分かれるイメージだったため)が、クライアントサイドのJavaScriptが動的にスタイルを変えてくれるのは、この手のスタイル変更がフロントエンド開発に欠かせなくなっていた当時は大助かりだったな〜と思っています。

この議論はいつの間にか見かけなくなりましたが、結果としてはCSS-in-JSに軍配が上がった気がします(この波を受けてなのか、開発の限界があったのかはわかりませんが、CSS Modulesをサポートする各所有名なライブラリがメンテナンスになったり非奨励になったりしていますし……)。

とはいえCSS Modulesを利用した開発というのは、個人的には今でも小さな開発であれば有用な気がしています(メンテナンスされていないのだけ怖いけれど)。

昔: React Server Componentの台頭

そんなこんなしている時にReact界隈に何度目かの大きなパラダイムシフトが起こりました。それがReact Server Component、いわゆるRSC周辺の大規模仕組みの変更によりCSS-in-JS界隈には激震が走ります(そんな気がした)。

というのも、React Server ComponentとCSS-in-JSの仕組みは非常に相性が悪いといわざるを得ません。JavaScriptの中で動かすということなので、ライブラリの内部ではReactのContext APIやwindow,documentといったクライアントサイドでしか動かないAPIの類を利用していることが非常に多かったためです。

<XXXProvider>{children}</XXXProvider>という類で要素をラップするようなライブラリはその中でReactのクライアントでしか動かないAPIが入っている可能性が高いですし、CSS-in-JSのメディアクエリについてはJavaScriptのAPIで判定しているところが多いです。

NextUIは、NextUIProviderにおいてcontet APIが使われています。

https://github.com/nextui-org/nextui/blob/1091377e4de83221068247ec6411e4af0082665b/packages/core/system/src/provider.tsx#L45

MUIのRadioGroupでも、内部でuseIdを利用しているのがわかります。

https://github.com/mui/material-ui/blob/8a34771a34f6e029caa73cc055d14a125123dc3b/packages/mui-material/src/RadioGroup/RadioGroup.js#L23

https://github.com/mui/material-ui/blob/8a34771a34f6e029caa73cc055d14a125123dc3b/packages/mui-material/src/RadioGroup/RadioGroup.js#L66

https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/useId/useId.ts#L33

結果的にどうなったかというと、UIライブラリを入れているところにはもれなくuse client;をつけまくる…という感じになりました。今はバージョンアップによりuse client;をつけなくても良くなったライブラリもありますが、ではどうしているかというとライブラリ側でuse client;をつけています。

MUIのApp Routerのサポート方法も、現在はこの手法になっています。

https://github.com/mui/material-ui/blob/8a34771a34f6e029caa73cc055d14a125123dc3b/packages/mui-material/src/RadioGroup/RadioGroup.js#L1

https://mui.com/blog/mui-next-js-app-router/

この方法でも良いですが、サーバーコンポーネント台頭の背景にはクライアントサイドのJavaScriptランタイム依存によるパフォーマンス低下を補う意図があります(サーバーで作って渡せば早くなるじゃん!みたいな)。そのため、クライアントコンポーネントでしか動かすことができないライブラリにはパフォーマンスの問題がともなうのでは?という懸念が上がるようになっていきます。

最近: ゼロランタイムCSS-in-JS

use client;を使いまくってクライアントコンポーネントを利用し続ける……ということによるパフォーマンス低下からの脱却を図るために登場したのが、ゼロランタイムCSS-in-JSという技術です。ビルド時に必要なCSSをすべて揃えることにより、クライアントコンポーネントにする必要をなくして、パフォーマンスを上げようという形ですね。

この手法は今もなお有用になっており、ゼロランタイムCSS-in-JSを利活用したライブラリに関する記事も多く、盛り上がっているように思えます。代表的なライブラリのコントリビュートもよくされている印象でした。UIライブラリの顔であるMUIもこのゼロランタイムCSS-in-JSの機構を入れていくと発表していますし。

ただ、元々のCSS-in-JSよりは規模も小さいため導入には検証が多く必要ですし、まだまだ発展していけそうな分野という感じな気がします。また、個人的には中身の仕組みについてまだわかりきっていないことが多いため、開発していくにあたりこの周辺の難しい問題にぶち当たった時の解消方法も難航しそうなイメージでした。

ピュアなCSSをサポートする機構のおすすめ

そこでふと、つまるところサーバーコンポーネントとCSS-in-JSの相性がなぜ悪かったのかといえば、JavaScriptの中でスタイリングしているからなのでは?と思いました。つまりピュアなCSSによるスタイリングの世界に行くことにより、クライアントやサーバーなどをスタイリング観点から意識しなくなって良いのかも、という感じです。

そもそもUIライブラリを使うのにuse client;を利用しなきゃいけないだなんて、内部構造を知っていなきゃわからないし、直感的ではないですよね。直感的でない開発体験にもなりかねないため、JavaScriptという世界線からスタイリングは分離しようという形です(CSS Modulesのように)。

TailwindCSSの大好きなところ

そこで推せるのが、TailwindCSSです。

ちなみに私は元々TailwindCSSというUIフレームワークが大好きです。これまでこのUIフレームワークが好きな理由は、完成されたUIライブラリよりも自由なスタイリングが可能だから、という意味にとどまっていました。ただ、今回色々と調べてみて、最近のReactとの相性やパフォーマンスという観点からも、改めてこのピュアなCSSを軸とするTailwindCSSはとても良いのだなと思えました。

https://tailwindcss.com/

スタイリングの観点からクライアント・サーバーコンポーネントを意識しなくて良いというのはとても良いところですよね。CSS ModulesにあったJavaScriptに直接CSSを埋め込めないことで起こる問題についても、ビルド時に実行されるtailwind.config.jsの設定周りの柔軟性がよくよくカバーしてくれている気がしており、私は今のところ開発をしていて課題を感じたことはありません(すごく複雑なCSSを作る時にはどうなるかわかりませんが……)。

また、TailwindCSSはビルド時に利用していないスタイルをすべて削除してくれるので、パフォーマンス的にサイズが軽量なのも良い点です。

TailwindCSSを利用する際にはほぼCSSのプロパティの知識が必須になるため、最新のCSSプロパティの勉強をするのにも役に立ちます(あんまり意識することないのですが、CSSもすごいスピードで進化していますよね。新しいプロパティもよく出ていますし)。SPやPCを出し分けるメディアクエリについてもJavaScriptではなくCSSの仕組みで動きます。

カラーパレットなどのグローバルなスタイル統一に関しても、かなり強力です(このあたりはCSS Modulesのツラミとしてもあったところかなと思っています)。

tailwind.config.ts
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        green: "var(--green)",
        yellow: "var(--yellow)",
        gray: "var(--gray)",
        deepgray: "var(--deepgray)",
        cream: "var(--cream)",
        black: "var(--black)",
        white: "var(--white)",
      },
    },
  },
  plugins: [],
};
export default config;
globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --gray: #f4f4f4;
  --black: #070707;
  --green: #315d55;
  --yellow: #f8cd43;
  --cream: #fff3cd;
  --deepgray: #babab1;
  --white: #ffffff;
}
layout.tsx
import type { ReactNode } from "react";
import "./globals.css";


export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ja">
      <body>
        {children}
      </body>
    </html>
  );
}

この設定によりtext-greenbg-yellowというクラスで指定した色味が反映されます。前述の通りtailwind.config.tsがビルド時にCSS変数を定義してくれているため、ここでも無論クライアント・サーバーコンポーネントを意識する必要がありません。

むしろ、サーバーコンポーネントと利用することによりメリットがある場合もあります。私の開発の場合、Next.jsのGoogleフォントを利用する際に、とても良いことがありました。

Next.jsのGoogleフォント利用方法について、詳しくはこちらにあります。
https://nextjs.org/docs/pages/building-your-application/optimizing/fonts#google-fonts

tailwind.config.ts
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        green: "var(--green)",
        yellow: "var(--yellow)",
        gray: "var(--gray)",
        deepgray: "var(--deepgray)",
        cream: "var(--cream)",
        black: "var(--black)",
        white: "var(--white)",
      },
+     fontFamily: {
+       wind: ["var(--font-windSong)"],
+     },
    },
  },
  plugins: [],
};
export default config;
app/font.ts
import { WindSong } from "next/font/google";

export const windSong = WindSong({
  subsets: ["latin"],
  weight: "400",
  variable: "--font-windSong",
});
app/layout.ts
+ import { windSong } from "./fonts";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ja">
-     <body>
+     <body className={`${windSong.variable}`}>
        {children}
      </body>
    </html>
  );
}
Sample.tsx
import type { FC } from "react";

export const Sample: FC = () => {
  return (
    <p className="font-wind">Hello, World</p>
  )
}

おしゃれなフォントが表示できました!

Hello,WorldでGoogleフォントになっている様子

ポイントはlayout.tsxをサーバーコンポーネントとして定義できるところです。CSSではビルド時に変数としてフォントを定義しておきつつ、サーバー側で完全にGoogleフォントを読み込んでからクライアントで表示できることにより、一瞬だけPCローカルなフォントがちらついて表示されてしまうFOUTの現象をおさえることができました!

TailwindCSS + HeadlessUIが最強と感じた理由

とはいえピュアなCSSに近いTailwindCSSを利用していると、痒いところに手が届きづらいなと思うことの1つにセマンティックHTMLやアクセシビリティといった観点があります。私は業務でMUIをヘビーに利用していたこともあり、MUIのアクセシビリティが考慮された美しいUIは、本当に素晴らしいなと思っていました。このあたりのサポートも、欲を言うと欲しいところです。

こうした要望については、HeadlessUIを利用するのが良さそうなのでとてもおすすめです!

https://headlessui.com/

HeadlessUIはスタイルを完全に除いてロジックのみを提供してくれるUIコンポーネントライブラリです。

Sample.tsx
import { Button } from "@headlessui/react";

export const Sample: FC = () => {
  return (
    <Button>Click me</Button>
  )
}

こんな感じで入力すると<button type="button" data-headlessui-state="">Click me</button>というHTMLが生成されます。無駄なスタイルは一切が省かれつつ、ボタンタグやtypeタグがきちんとついたセマンティックHTMLになっています。便利ですね……。

HeadlessUIはロジックを提供するUIコンポーネントというだけなので、もちろん内部でJavaScriptAPIが含まれていて……なんてことはありません。TailwindCSSと同じく、このUIコンポーネントを利用するからクライアント・サーバーコンポーネントになる、みたいな脳みそを使う必要がなくなるわけですね(とはいえフォームなどのUIを扱う際には、自前で状態を管理するReactのAPIを使うことになるでしょうし、その時はuse client;になってくるはずです)。

ちなみにTailwindCSSとHeadlessUIをスタイリングに利用する場合、@storybook/nextjsとの連携もいい感じでサクッと行うことができました。プレビューにTailwindCSSの読み込みを済ませているglobals.cssをインポートしつつ、Googleフォントの変数を定義するだけです。

.storybook/preview.tsx
import type { Preview } from "@storybook/react";
+import "../app/globals.css";
+import { windSong } from "@/app/fonts";

const preview: Preview = {
+ decorators: [
+   (Story) => (
+     <div
+       className={`${windSong.variable}`}
+     >
+       <Story />
+     </div>
+   ),
+ ],
};

export default preview;

おわりに

最近のReactにとって良さそうなスタイリングってどういう形だろうか?というのを考えた結果について記事にしてみましたが、いかがだったでしょうか?

個人的にはまだまだゼロランタイムCSS-in-JSについては使ってみた経験があまりないので日和っていたり、複雑なUIを作るのにTailwindCSSを使い尽くしていなかったりするので、そのあたりの経験が伴っていれば全く変わってくるのかなと思いましたが、一旦は今の所感として置かせてもらいました。

冒頭申し上げた通り私はスタイリングがとても好きなので、痒いところ(ブラウザごとのベンダープレフィックスやアクセシビリティ、セマンティックHTMLなど)に手が届きつつも、自分のしたい表現を邪魔しないTailwindCSS + HeadlessUIという構成は、とてもバランスが良いと感じました。

サーバーコンポーネントの台頭によりJavaScriptで色々するということが少々複雑になってきた今、CSSはCSSでピュアに書くということは、とても良い選択肢なのかなと思えました。

ここまで読んでいただき、ありがとうございました!

Discussion

MeguriMeguri

TailwindCSSやReactのスタイリング方法の進化についての考察がとても面白かったです。今後もこのテーマに関する記事を楽しみにしています