POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSXFacebook
Josh W. Comeau

本記事は、原著者の許諾のもとに翻訳・掲載しております。

2023年5月4日、VercelはNext 13.4の安定版リリースを発表し、React Server Componentsを基盤として構築された初のReactフレームワークとなりました。

これは非常に大きな出来事です!RSC(React Server Components)は、Reactにおいてサーバー専用のコードを記述するための公式な方法を提供してくれます。私のブログ記事「Making Sense of RSC(RSCを理解する)」でも書いたように、RSCは多くの興味深い新たな扉を開いてくれます。

しかし、オムレツを作るには卵を割らなければなりません。 RSCはReactの仕組みに対する根本的な変更です。その影響で、私たちが使ってきたライブラリやツールの一部は…スクランブルエッグのようにごちゃ混ぜになってしまいました 😅。styled-componentsやEmotionのようなライブラリを使用している私たちにとって、今後の明確な道筋は存在していませんでした。

過去数ヶ月間、私はこの問題について深く掘り下げ、互換性の問題についての理解を深め、どのような選択肢があるのかを学んできました。現時点で、私は状況全体をかなりしっかりと把握できていると感じています。

また、水面下で進んでいた非常にエキサイティングな動向もいくつか発見しました。 ✨

CSS-in-JSライブラリを使用しているなら、この記事が多くの疑問を解消し、今後の具体的な選択肢を提示する手助けになれば幸いです。

ただ____を使えばいい。

ネット上でこの議論が持ち上がると、最もよくある提案の1つは、別のCSSツールに切り替えることです。結局のところ、Reactエコシステムには選択肢が山ほどあるのですから!

しかし、私たちの多くにとって、これは現実的な提案ではありません。私は自分のブログとコースプラットフォーム全体で、5,000以上のstyled-componentを使用しています。まったく別のツールへ移行するというのは、口で言うほど簡単なことではありません。

LLM(大規模言語モデル)の登場により、この種の大規模なリファクタリングは潜在的には実行可能になりますが、それでも多くの手作業による検証が必要になるでしょう。そして正直なところ、たとえ指を鳴らすだけで即座に別のライブラリに切り替えることができたとしても、私はそうしたくありません。私はstyled APIが本当に好きなのです!

このブログ記事の後半では、いくつかの代替CSSライブラリについて説明しますが、ここではstyled-componentsに似たAPIを持つ選択肢に焦点を当てます。

2026年のアップデート:styled-componentsが復活しました!

2026年1月、styled-componentsチームはv6.3.0を公開し、React Server Componentsのサポートを導入しました! 🎉

これは正直言って私にとって非常にエキサイティングなことです。 昨年、styled-componentsが最小限の体制(人員)で「メンテナンスモード」に入ったとき、私はライブラリに大きなアップデートはないだろうと思い込んでいましたが、それは間違っていました!つい先週、リードメンテナーのEvan Jacobs氏が、このライブラリが現在も活発にメンテナンスされていることを明言しました(別タブで開きます)

React Server Componentsのコンテキストでstyled-componentsを使用する方法については、こちらで学べます:

React Server Componentsを紐解く

互換性の問題を理解するためには、React Server Componentsについて理解する必要があります。しかし、その話をする前に、サーバーサイドレンダリング(SSR)について理解しているか確認する必要があります。

SSRは、いくつかの異なる戦略や実装を含む総称ですが、最も典型的なバージョンは次のようになります:

  1. ユーザーが私たちのウェブアプリにアクセスする。
  2. リクエストはNode.js(ブラウザを持たないサーバーランタイム)によって受信され、そこでReactが実行される。アプリケーションがレンダリングされ、初期UIのすべてを含む完全に形成されたHTMLドキュメントが生成される。
  3. このHTMLドキュメントがユーザーのデバイスに読み込まれると、Reactはまったく同じコンポーネントをすべて再レンダリングし、サーバーで行われた作業を繰り返す。ただし、新しいHTML要素を生成するのではなく、サーバーによって生成された既存のHTML要素を「引き継ぐ(adopt)」(*)。これはハイドレーション(hydration)として知られている。

*ここでは「HTML要素」という用語を使用していますが、より正確な用語は「DOMノード」です。サーバーがHTMLを生成し、ブラウザがそれをDOMとして知られる動的な表現にレンダリングします。

Reactは、対話性(インタラクティビティ)を処理するためにユーザーのデバイス上で実行される必要があります。サーバーによって生成されたHTMLは完全に静的です。そこには私たちが記述したイベントハンドラ(例:onClick)は含まれず、私たちが指定した参照(ref属性による)もキャプチャされていません。

ではなぜ、まったく同じ作業をすべてやり直さなければならないのでしょうか? Reactがユーザーのデバイスで起動したとき、既存のUIの集まりを発見します。しかし、どのコンポーネントがどのHTML要素を所有しているかといったコンテキスト(文脈)は一切持っていません。Reactは、コンポーネントツリーを再構築するためにまったく同じ作業をする必要があります。これにより、既存のHTMLを正しく結びつけ、イベントハンドラや参照を正しい要素にアタッチできるようになります。

Reactは、サーバーの処理が終わった時点からスムーズに引き継げるよう、内部でコンポーネントツリーの対応関係(マップ)を構築する必要があるのです。

このモデルには大きな制限が1つあります。 私たちが書くコードはすべて、サーバークライアントの両方で実行されます。サーバー上でのみ排他的にレンダリングされるコンポーネントを作成する方法はありません。

データベースにデータを持つ、フルスタックのウェブアプリケーションを構築していると仮定しましょう。もしあなたがPHPなどの言語の経験者であれば、次のような書き方ができると期待するかもしれません:

function Home() {
  const data = db.query('SELECT * FROM SNEAKERS');

  return (
    <main>
      {data.map(item => (
        <Sneaker key={item.id} item={item} />
      ))}
    </main>
  );
}

理論的には、このコードはサーバー上で問題なく動作する可能性がありますが、そのまったく同じコードがユーザーのデバイス上で再実行されます。クライアント側のReactは私たちのデータベースにアクセスできないため、これは問題となります。Reactに対して「このコードはサーバー上でのみ実行し、結果のデータをクライアントで再利用してくれ」と伝える方法はありません。

Reactの上に構築されたメタフレームワークは、独自の解決策を考え出しました。例えば、Next.jsでは次のようなことができます:

export async function getServerSideProps() {
  const data = await db.query('SELECT * FROM SNEAKERS');

  return {
    props: {
      data,
    },
  };
}

function Home({ data }) {
  return (
    <main>
      {data.map(item => (
        <Sneaker key={item.id} item={item} />
      ))}
    </main>
  );
}

Next.jsチームはこう言いました。「よし、まったく同じReactのコードがサーバーとクライアントで実行されなければならないことは分かった…しかし、Reactの外部に、サーバー上でのみ実行される追加のコードを加えることができるぞ!」。

Next.jsサーバーがリクエストを受信すると、まずgetServerSideProps関数を呼び出し、そこから返された値はすべて、propsとしてReactコードに渡されます。 まったく同じReactコードがサーバーとクライアントで実行されるため、問題はありません。なかなか賢いやり方ですよね?

私は正直、今日でもこのアプローチの大ファンです。しかし、Reactの制限による必要性から作成されたAPIであり、少し回避策のように感じられることも事実です。

また、各ルートの最上部にあるページレベルでしか機能しません。好きな場所にgetServerSideProps関数を自由に配置できるわけではないのです。

React Server Componentsは、この問題に対してより直感的な解決策を提供します。 RSCを使用すると、データベース呼び出しなどサーバー専用の作業をReactコンポーネント内で直接行えます:

async function Home() {
  const data = await db.query('SELECT * FROM SNEAKERS');

  return (
    <main>
      {data.map(item => (
        <Sneaker key={item.id} item={item} />
      ))}
    </main>
  );
}

「React Server Components」のパラダイムでは、コンポーネントはデフォルトでServer Components(サーバーコンポーネント)になります。サーバーコンポーネントはサーバー上でのみ実行されます。このコードはユーザーのデバイスでは再実行されません。コードはJavaScriptバンドルに含まれることすらありません!

この新しいパラダイムにはClient Components(クライアントコンポーネント)も含まれています。クライアントコンポーネントは、サーバーとクライアントの両方で実行されるコンポーネントです。あなたが「従来の」(RSC以前の)Reactで書いてきたすべてのReactコンポーネントは、クライアントコンポーネントです。つまり、既存の概念に新しい名前がついただけなのです。

ファイルの先頭に新しい"use client"ディレクティブを記述することで、明示的にクライアントコンポーネントとして指定(オプトイン)します:

'use client';

function Counter() {
  const [count, setCount] = React.useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

このディレクティブは「クライアント境界(client boundary)」を作成します。このファイル内のすべてのコンポーネントとインポートされたコンポーネントは、クライアントコンポーネントとしてレンダリングされます。最初にサーバーで、次にクライアントで再び実行されます。

他のReact機能(例:フック)とは異なり、React Server Componentsはバンドラーとの深い統合を必要とします。私がこれを書いている2024年4月現在、React Server Componentsを使用する唯一の現実的な方法はNext.jsを使うことです。ただし、将来的にはこれも変わると予想しています。

サーバーコンポーネントには制限がある

サーバーコンポーネントについて理解しておくべき重要な点は、これらが「完全な」React体験を提供するわけではないということです。ほとんどのReact APIはサーバーコンポーネントでは機能しません。

例えば、useStateです。状態(state)変数が変更されると、コンポーネントは再レンダリングされますが、サーバーコンポーネントは再レンダリングできません。そのコードは決してブラウザに送信されないため、Reactは状態の変更をどのように処理すればよいか全く分かりません。Reactの観点から見ると、サーバーコンポーネントによって生成されたマークアップは確定しており、クライアントで変更できません(*)。

*誤解のないように言っておくと、DOM自体が不変なわけではありません。プレーンなJavaScriptを使用して変更は可能です。しかし、Reactはサーバーコンポーネントによって生成されたアプリケーションの部分を再レンダリングはできません。

同様に、サーバーコンポーネント内でuseEffectを使用できません。エフェクトはサーバー上で実行されず、クライアントでのレンダリング後にのみ実行されるからです。そして、サーバーコンポーネントはJavaScriptバンドルから除外されるため、クライアント側のReactは私たちがuseEffectフックを記述したことを知る由もありません。

useContextフックでさえ、サーバーコンポーネントの内部では使用できません。なぜなら、Reactコンテキストをサーバーコンポーネントとクライアントコンポーネントの両方でどのように共有するかという問題を、Reactチームがまだ解決していないからです。

私はこのように考えています: サーバーコンポーネントは、少なくとも私たちが従来理解してきたような意味において、本当のReactコンポーネントではありません。オリジナルのHTMLを作成するためにサーバーによってレンダリングされる、PHPテンプレートにずっと近いものです。本当の革新は、サーバーコンポーネントとクライアントコンポーネントが同じアプリケーション内に共存できることなのです!

さらに深く掘り下げる

このブログ記事では、React Server Componentsの最も関連性の高い詳細、つまりCSS-in-JSフレームワークとの互換性の問題を理解するために知っておくべきことに焦点を当てます。

しかし、もしReact Server Componentsについてもっと詳しく学びたいのであれば、この新しい世界をより深く探求した別のブログ記事があります:

CSS-in-JSライブラリの仕組み

さて、React Server Componentsの基本についてカバーしました。次に、💅 styled-componentsのようなCSS-in-JSライブラリの基本について話しましょう!

簡単な例を挙げます:

import styled from 'styled-components';

export default function Homepage() {
  return (
    <BigRedButton>
      Click me!
    </BigRedButton>
  );
}

const BigRedButton = styled.button`
  font-size: 2rem;
  color: red;
`;

.red-btnのようなクラスにCSSを記述する代わりに、新しく生成されたReactコンポーネントにそのCSSをアタッチします。これがstyled-componentsを特別なものにしている理由です。クラスではなく、コンポーネントが再利用可能なプリミティブ(基本要素)なのです。

styled.buttonは、新しいReactコンポーネントを動的に生成してくれる関数であり、そのコンポーネントをBigRedButtonという変数に割り当てています。その後、他のReactコンポーネントを使用するのと同じ方法で、このReactコンポーネントを使用できます。これにより、大きな赤いテキストを持つ<button>タグがレンダリングされます。

しかし、ライブラリは具体的にどのようにしてこのCSSをこの要素に適用するのでしょうか?主に3つの選択肢があります(*):

*さらに、CSSオブジェクトモデル(CSSOM)を通じて動的にスタイルを追加できますが、これはSSR中には選択できないため、ここでは含めていません。

  1. スタイルは、style属性を通じてインラインで適用できる。
  2. スタイルは、個別のCSSファイルに記述し、<link>経由で読み込める。
  3. スタイルは、<style>タグ内に配置できる。通常は現在のHTMLドキュメントの<head>内に配置される。

このコードを実行してDOMを検査すると、答えが明らかになります:

<html>
  <head>
    <style data-styled="active">
      .abc123 {
        font-size: 2rem;
        color: red;
      }
    </style>
  </head>

  <body>
    <button className="abc123">
      Click me!
    </button>
  </body>
</html>

styled-componentsは、提供されたスタイルをライブラリが管理する<style>タグに書き込みます。これらのスタイルをこの特定の<button>に紐付ける(適用する)ために、"abc123"という一意のクラス名を生成します。

これらの作業はすべて、最初のReactレンダリング時に行われます:

  • クライアントサイドレンダリングのコンテキスト(例:Parcel、create-react-app)では、Reactが作成する他のDOMノード同様、<style>タグもデバイス上で動的に生成される。
  • サーバーサイドレンダリングのコンテキスト(例:Next、Remix)では、この作業はサーバー上で行われる。生成されたHTMLドキュメントには、この<style>タグが含まれる。

ユーザーがアプリケーションを操作するにつれて、特定のスタイルを作成、変更、または破棄する必要が生じる場合があります。例えば、条件付きでレンダリングされるstyled-componentを考えてみましょう:

function Header() {
  const user = useUser();

  return (
    <>
      {user && (
        <SignOutButton onClick={user.signOut}>
          Sign Out
        </SignOutButton>
      )}
    </>
  );
}

const SignOutButton = styled.button`
  color: white;
  background: red;
`;

最初は、userが未定義であれば<SignOutButton>はレンダリングされないため、これらのスタイルは存在しません。後でユーザーがログインすると、アプリケーションが再レンダリングされ、styled-componentが作動して、これらのスタイルを<style>タグに注入します。

基本的に、すべてのstyled componentは通常のReactコンポーネントですが、ちょっとした副作用も併せ持っています:それは、自身のスタイルも<style>タグにレンダリングするということです。

本記事の趣旨としては、これが最も重要なポイントです。ライブラリの内部動作についてさらに深く掘り下げたい場合は、「Demystifying styled-components(styled-componentsの謎を解く)」というブログ記事をご参照ください。

問題の核心

これまでに学んだことを要約すると:

  • 「React Server Components」はReactの新しいパラダイムであり、新しいタイプのコンポーネントであるサーバーコンポーネントを提供する。サーバーコンポーネントはサーバー上でのみレンダリングされる。そのコードは、クライアントに送信されるJSバンドルに含まれることすらない。
  • styled-componentsライブラリは、CSSがアタッチされたReactコンポーネントを動的に作成できるようにする。これは、コンポーネントが再レンダリングされる際に更新される<style>タグを管理することによって機能する。

根本的な非互換性は、styled-componentsがブラウザ内で実行されるように設計されているのに対し、サーバーコンポーネントは決してブラウザに触れないという点にあります。

styled-componentsは内部でuseContextフックに大きく依存しています。Reactのライフサイクルに組み込まれることを意図していますが、サーバーコンポーネントにはReactのライフサイクルが存在しません。そのため、この新しいRSCの世界でstyled-componentsを使用したい場合は注意が必要です。styled-componentを1つでも使用しているコンポーネントは、すべてクライアントコンポーネントにする必要があります。

皆さんはどうか分かりませんが、私の場合、スタイリングを一切含まないReactコンポーネントを作ることはかなり稀です。私のコンポーネントファイルの90%以上がstyled-componentsを使用していると見積もっています。これらのコンポーネントの大部分は、それ以外は完全に静的です。状態やその他のクライアントコンポーネントの機能は使用していません。

この新しいパラダイムの利点を最大限に活かせないということなので、これは確かに残念なことです…しかし、だからといって絶望するほどのことではありません。

もし私がReact Server Componentsについて1つだけ変えることができるとしたら、それは「クライアントコンポーネント」という名前でしょう。この名前は、これらのコンポーネントがクライアントでのみレンダリングされることを示唆していますが、それは事実ではありません。覚えておいてください、「クライアントコンポーネント」とは古いものに対する新しい名前なのです。2023年5月以前に作成されたすべてのReactアプリケーションにおける、すべてのReactコンポーネントはクライアントコンポーネントです。

styled-componentsアプリケーションでは、コンポーネントの10%しかサーバーコンポーネントにできないかもしれません。それでも改善であることに変わりはありません! 私たちのアプリケーションは、RSC以前の世界よりも少し軽く、速くなるでしょう。私たちは依然としてSSRのすべての恩恵を受けています。それは変わっていません。

2026年 アップデート

前述の通り、styled-componentsは最近更新され、React Server Componentsをサポートするようになりました。これは、<style>タグをホイスト(巻き上げ)し、重複を排除する新しいReact機能によって可能になりました。

幸いなことに、サーバーコンポーネントはクライアントコンポーネントに切り替える必要なくstyled-componentsを使用できるようになりました。開発チームはこのアップデートで見事な対応をしてくれました。 ❤️

ゼロランタイムCSS-in-JSライブラリの世界

React Server Componentsが登場する以前から、CSS-in-JSの領域では、作業をより早い段階へ移し、ランタイム(実行時)ではなくコンパイル時にスタイルを抽出する動きがありました。

最新のReactアプリケーションにはビルドステップがあり、そこでTypeScript/JSXをJavaScriptに変換し、何千もの個別のファイルを少数のバンドルにパッケージ化します。この作業は、アプリケーションがデプロイされたとき、本番環境で実行を開始する前に行われます。ランタイム(実行時)ではなく、このステップの間にstyled componentを処理してはどうでしょうか?

これが、このセクションで議論するすべてのライブラリの背後にある核となるアイデアです。さっそく見ていきましょう!

Linaria

Linaria(別タブで開きます)はずっと前、2017年に作成されました。styled-componentsとほぼ同じくらい古いです!

APIはstyled-componentsとまったく同じに見えます:

import { styled } from '@linaria/react';

export default function Homepage() {
  return (
    <BigRedButton>
      Click me!
    </BigRedButton>
  );
}

const BigRedButton = styled.button`
  font-size: 2rem;
  color: red;
`;

ここで非常に巧妙なのが以下の点です: コンパイルステップの間に、Linariaはこのコードを変換し、すべてのスタイルをCSS Modules(別タブで開きます)に移動します。

Linariaを実行した後、コードは次のようになります:

/* /components/Home.module.css */
.BigRedButton {
  font-size: 2rem;
  color: red;
}
/* /components/Home.js */
import styles from './Home.module.css';

export default function Homepage() {
  return (
    <button className={styles.BigRedButton}>
      Click me!
    </button>
  );
}

CSS Modulesにまだ馴染みがない方のために説明すると、これはCSSに対する軽量な抽象化です。ほとんどプレーンなCSSとして扱うことができますが、グローバルに一意な名前を気にする必要はありません。コンパイルステップの間に、Linariaが魔法をかけた直後、.BigRedButtonのような一般的な名前は.abc123のような一意な名前に変換されます。

重要なのは、CSS Modulesがすでに広くサポートされているということです。 存在する中で最も人気のある選択肢の1つです。Next.jsのようなメタフレームワークは、すでにCSS Modulesをファーストクラスでサポートしています。

そのため、車輪を再発明し、堅牢で本番環境に対応できるCSSソリューションを構築するのに何年も費やすのではなく、Linariaチームは近道を取ることにしました。私たちがstyled-componentsを書くと、Linariaがそれを事前処理してCSS Modulesにし、それがさらにプレーンなCSSに処理されます。これらはすべてコンパイル時に行われます。

ランタイム vs コンパイル時のトレードオフ

RSCが存在するずっと前から、コミュニティはLinariaのようなコンパイル時のライブラリを構築してきました。パフォーマンス上の利点は明白です:styled-componentsはJavaScriptバンドルに11キロバイト(gzip)を追加しますが、Linariaは事前にすべての作業を完了させるため0kbです。さらに、スタイルの収集・適用に時間を費やす必要がなく、サーバーサイドレンダリングも少し速くなります。

とは言え、styled-componentsのランタイムは単なるお荷物というわけではありません。コンパイル時には不可能なことをstyled-componentsで行うことができます。例えば、styled-componentsはReactの状態の一部が変更されたときにCSSを動的に更新できます。

幸いなことに、styled-componentsが最初に作成されてからの10年近くで、CSSははるかに強力になりました。ほとんどの動的なユースケースをCSS変数で処理できます。最近では、ランタイムを持つことが特定の状況下で少し良い開発者体験(DX)を提供できる場合もありますが、私の意見では、もはや本当に必要なものではありません。

これは、Linariaやその他のコンパイル時のCSS-in-JSライブラリが、真の意味でstyled-componentsやEmotionにそのまま置き換えられる代替品にはならないということです。動的なコンポーネントを作り直すのにいくらかの時間を費やす必要があります。しかし、これは全く異なるCSSツールに切り替えることと比較すれば、ほんのわずかな作業にすぎません。

Linariaへの移行

では、私たちは皆、自分のstyled-componentsアプリケーションをLinariaに移行すべきなのでしょうか?

私が2024年にブログを再構築した(別タブで開きます)際、styled-componentsからLinariaに切り替えることを決めました。私のブログはNext.jsを使用しており、統合を管理するパッケージであるnext-with-linaria(別タブで開きます)を見つけました。

このアプローチは私にとって十分うまく機能しましたが、途中で多くの癖に悩まされました。そしておそらく最大の問題は、LinariaとNext.jsバインディングの両方がかなりニッチであるということです。このスタックを使用する開発者の大規模なコミュニティが存在しないため、問題に遭遇した際に行き詰まりを解消するリソースが少ないのです。

もしあなたが経験豊富な開発者であり、問題をデバッグするためにNPMパッケージを掘り下げることを気にしないのであれば、このスタックは実際にはかなり素晴らしいものでしょう。しかし、ほとんどの状況において本当にお勧めできるものではありません。

Panda CSS

スケートボードとタピオカティーを持ったかわいいパンダ、Panda CSSのマスコット

Panda CSS(別タブで開きます)は、人気のあるコンポーネントライブラリであるChakra UIを構築した人々によって開発された最新のCSS-in-JSライブラリです。

Panda CSSには多くの異なるインターフェースが付属しています。Tailwindのように使用し、mb-5のような省略形のクラスを指定できます。Stitchesのように使用し、バリアントやcvaを使用できます。あるいは、styled-componentsのように使用できます。

styled APIを使用した場合は次のようになります:

import { styled } from '../styled-system/jsx'

export default function Homepage() {
  return (
    <BigRedButton>
      Click me!
    </BigRedButton>
  );
}

const BigRedButton = styled.button`
  font-size: 2rem;
  color: red;
`;

Linariaと同様に、Panda CSSもコンパイル時に処理されランタイムコードは取り除かれますが、代わりにTailwindスタイルのユーティリティクラスへとコンパイルされます。最終的な結果は次のようになります:

/* /styles.css */
.font-size_2rem {
  font-size: 2rem;
}
.color_red {
  color: red;
}
/* /components/Home.js */
export default function Homepage() {
  return (
    <button className="font-size_2rem color_red">
      Click me!
    </button>
  );
}

color: redのような一意のCSS宣言ごとに、Panda CSSは1つの中央CSSファイルに新しいユーティリティクラスを作成します。その後、このファイルが私たちのReactアプリケーションのすべてのルートで読み込まれることになります。

個人的には、Panda CSSをぜひ採用したいところです。関連する経験を豊富に持つ堅実なチームによって開発されており、使い慣れたAPIを提供し、かわいいスケートボードに乗るパンダのマスコットまでいるのですから!

しかし、実験してみた結果、これは私には合わないということに気づきました。私の抱える問題のいくつかは、取るに足らない/表面的なものです。例えば、Panda CSSはプロジェクト内に大量のファイルを生成し、ディレクトリが散らかります。これは私には少し厄介に感じられますが、最終的には重大な問題ではありません。

私にとってのより大きな問題は、Panda CSSに重要な機能が欠けていることです。コンポーネントを簡単に相互参照(クロスリファレンス)できないのです。

これは少し高度なトピックであり、ここで取り上げるには話が逸れすぎます。別のブログ記事「The styled-components Happy Path(styled-componentsの幸せな道)」でこのパターンを共有しています。

核となるアイデアは、文脈に応じた(contextual)スタイルを含め、特定のコンポーネントに対するすべてのスタイルを1箇所にまとめられるべきだということです。例えばTextLinkコンポーネントなら、デフォルトのスタイルを指定できるだけでなくAsideQuote内でレンダリングされた場合のオーバーライド(上書き)も指定できるべきです:

import Link from 'next/link';

import { AsideWrapper } from '@/components/Aside';
import { QuoteWrapper } from '@/components/Quote';

const TextLink = styled(Link)`
  /* デフォルトのスタイル */
  color: var(--color-primary);
  text-decoration: none;

  /* TextLinkがAside内にある場合のオーバーライド */
  ${AsideWrapper} & {
    color: inherit;
    text-decoration: underline;
  }

  /* TextLinkがQuote内にある場合のオーバーライド */
  ${QuoteWrapper} & {
    font-weight: var(--font-weight-bold);
    color: var(--color-secondary);
  }
`;

悲しいことに、このパターンはPanda CSSではうまく機能しません。上で述べたように、Panda CSSは各コンポーネントに対して一意のクラス名を生成するのではなく、ユーティリティクラスを生成します。その結果、このようにコンポーネントを相互参照するための組み込みの方法が存在しないのです(*)。

*データ属性を使用して自分たちで無理やり実装はできますが、実装のハードルがはるかに高くなるため、現実的にこのパターンを使い続ける人がいるとは思えません。

もしあなたがこのパターンに興味がないのであれば、Panda CSSはあなたのアプリケーションにとって良い選択肢になるでしょう!しかし私にとって、これは採用を見送る決定的な理由となります。

Pigment CSS

最も人気のあるReactコンポーネントライブラリの1つであるMaterial UI(別タブで開きます)は、Emotionの上に構築されています(*)。彼らの開発チームは、RSCの互換性を巡るまったく同じ問題に取り組んでおり、それについて何か行動を起こすことを決定しました。

*このライブラリはstyled-componentsに切り替えるオプションも提供しています。

彼らは最近、新しいライブラリをオープンソース化しました。Pigment CSS(別タブで開きます)という名前です。そのAPIは、この時点でかなり馴染みのあるものに見えるはずです:

import { styled } from '@pigment-css/react';

export default function Homepage() {
  return (
    <BigRedButton>
      Click me!
    </BigRedButton>
  );
}

const BigRedButton = styled.button`
  font-size: 2rem;
  color: red;
`;

Pigment CSSはコンパイル時に実行され、Linariaと同じ戦略を使用し、CSS Modulesにコンパイルします。Next.jsとViteの両方用のプラグインがあります。

実際、これはWyW-in-JS(別タブで開きます)("What you Want in JS":JSであなたが望むもの)という低レベルのツールを使用しています。このツールはLinariaのコードベースから進化し、「CSS Modulesへのコンパイル」のビジネスロジックを分離して汎用化しました。これにより、Pigment CSSのようなライブラリがその上に独自のAPIを構築できるようになりました。

正直に言って、これは私にとって完璧な解決策のように感じられます。CSS Modulesはすでに十分に実戦テストされ、高度に最適化されています。そして、私がこれまでに見た限りでは、Pigment CSSは優れたパフォーマンスとDX(開発者体験)を備えた素晴らしいものです。

Material UIの次のメジャーバージョンはPigment CSSをサポートし、最終的にはEmotion/styled-componentsのサポートを完全に打ち切る計画です。結果として、Pigment CSSは最も広く使用されるCSS-in-JSライブラリの1つになるでしょう。Material UIはNPMで週に約500万回ダウンロードされており、これはReact自体の約5分の1に相当します!

まだ非常に初期の段階です。Pigment CSSは2024年3月にオープンソース化されたばかりです。しかし、チームはこのプロジェクトに多大なリソースを投入しています。状況がどのように発展していくのか見るのが待ちきれません!

2026年 アップデート

このブログ記事は元々2024年4月に公開されましたが、その当時、Pigmentには多くの勢いがあるように見えました。残念ながら、その勢いは少し衰えてしまったようです。

私の知る限り、このライブラリはまだ安定版(stable)とは見なされておらず、NPMパッケージは1年以上更新されていません。

これが最良の移行オプションになるだろうと本当に思っていたので、非常に残念です。しかし、うまくいっていないようです。

まだまだ続く

これまでに取り上げたライブラリに加えて、エコシステムには興味深いことを行っているプロジェクトがさらに多く存在します。私が注目している他のプロジェクトをいくつか紹介します:

  • next-yak(別タブで開きます) — スイス最大のeコマース小売業者の開発者によって作成されたコンパイル時のCSS-in-JSライブラリ。二次的なAPIの多くを再実装し、可能な限りstyled-componentsのドロップイン代替となることを目指している。
  • Kuma UI(別タブで開きます) — このライブラリはかなり野心的なことに挑戦している。「ハイブリッド」設計であり、ほとんどのスタイルはコンパイル時に抽出されるが、クライアントコンポーネント用にランタイムも利用可能である。
  • Parcel macros(別タブで開きます) — Parcelは最近「マクロ(macros)」を実装したバンドラーである。マクロは、コンパイル時のCSS-in-JSライブラリを含むあらゆるものを構築するために使用できるツールである。驚くべきことに、この機能はParcel固有のものではなく、Next.jsと一緒に使用できる。

今後の道筋

さて、私たちは非常に多くの選択肢を検討してきましたが、依然として疑問は残ります。もしあなたが「レガシーな」CSS-in-JSライブラリを使用している本番アプリケーションを持っている場合、実際に何をすべきなのでしょうか?

少し直感に反しますが、多くの場合、私はあなたが実際に何かをする必要はないと考えています。 😅

オンラインでの多くの議論は、モダンなReact / Next.jsアプリケーションではstyled-componentsを使用できないかのように聞こえます。あるいは大きなパフォーマンス上のペナルティがあるかのようにも。しかし、それは本当ではありません。

多くの人々がRSC(React Server Components)とSSR(Server Side Rendering)を混同しているように感じます。サーバーサイドレンダリングは、これまでと全く同じように機能し続けており、これらの影響を受けることはありません。

NextのApp Routerやその他のRSC実装に移行したからといって、アプリケーションが遅くなるべきではありません。実際には、おそらく少し速くなるでしょう!

パフォーマンスの観点から見ると、RSCとゼロランタイムCSSライブラリによる主な利点は、TTI(Time To Interactive:操作可能になるまでの時間)です。これは、UIがユーザーに表示されてから、UIが完全にインタラクティブ(対話可能)になるまでの遅延です。これを軽視すると、ユーザーがボタンをクリックしても何も起こらない(裏側でアプリケーションのハイドレーションがまだ完了していないため)という悪いユーザー体験を生み出す可能性があります。

したがって、現在アプリケーションのハイドレーションに長い時間がかかっている場合は、ゼロランタイムCSSライブラリへの移行を主張する強力な根拠となります。しかし、アプリがすでに安定したTTIを持っている場合、ユーザーがおそらくこの移行から利益を得ることはないでしょう。

多くの場合、最大の問題はFOMO(Fear Of Missing Out:取り残される恐怖)でしょう。 開発者として、私たちは最新かつ最高のツールを使いたいものです。新しい最適化の恩恵を十分に受けていないと知りながら、たくさんの"use client"ディレクティブを追加するのは楽しいことではありません。しかし、これは本当に大規模な移行をするための説得力のある理由なのでしょうか?

2026年 アップデート

この結論は、styled-componentsがReact Server Componentsをサポートするように更新されることは決してないと思われていた2024年に書かれました。現在、styled-componentsが更新されたことで、答えはずっとシンプルになりました。

私は、styled-componentsが積極的にメンテナンスされている限り、これを使い続けるつもりです。 😄

そうは言っても、コミュニティの勢いがもはやstyled-componentsにないことは認めるべきでしょう。2023年、styled-componentsとTailwindはどちらもNPMから週に約600万回ダウンロードされていました。2026年初頭、styled-componentsは30%以上成長し週に約800万ダウンロードとなりました。一方Tailwindは週に約4500万ダウンロードへと急上昇しています(ソース(別タブで開きます))。

TailwindがReactアプリケーションのスタイリングにおける事実上の標準となったことに疑いの余地はありません。

しかし、styled-componentsを使用しており、Tailwindに切り替えるつもりが全くない開発者も依然として数多く存在します。そして、この立場にいる人々にとって、実行可能な代替手段があることは本当に大きな安堵です。 ✨

私がやっていること

私は2つの主要な本番アプリケーションを保守しています。1つはこのブログ、もう1つはインタラクティブなコース(CSS for JavaScript Developers(別タブで開きます) および The Joy of React(別タブで開きます))用のコースプラットフォームです。

私のコースプラットフォームは依然としてstyled-componentsと共にNext.js Pages Routerを使用しており、近い将来に移行する計画はありません。そのユーザー体験に満足しており、移行によるパフォーマンス上の大きな利点があるとは信じていません。

このブログ記事を最初に公開して以来、私はブログを移行しました。styled-componentsを備えたNext.js Pages Routerから、Linaria(別タブで開きます)およびnext-with-linaria(別タブで開きます)を備えたNext.js App Routerへの移行です。正直に言って、それはかなり期待外れでした。 😅

理論上は、RSCの採用とコンパイル時のCSS-in-JSの採用によって、ちょっとした嬉しいパフォーマンスの向上を見込めたはずでした。しかし実際には、パフォーマンスはわずかに悪化しました。その理由は正確には分かりませんが、1つの大きな理由は、Next.js App RouterがCSS Modulesを異なる方法で処理し、大量のスタイルが先回りして読み込まれてしまうことにあるようです。

これは後続の内部リンクのクリックを高速化する一方で、最初のリクエストを遅くします。このブログのようなプロジェクトにとっては理にかなったトレードオフではありません

React Server Componentsは超クールです。React/Vercelチームは、サーバー上でのReactの動作方法を見直すという素晴らしい仕事をしました。しかし正直なところ、自分自身でこれらの移行の1つに着手してみて、ほとんどの本番アプリケーションにこれを推奨できるかどうか確信が持てません。

全体として、開発者体験とユーザー体験の両方において、Pages + styled-componentsの方が優れていたと言えるでしょう。

もしあなたがアプリケーションのパフォーマンスに満足しているなら、アップデートや移行を急ぐ必要はないでしょう ❤️。エコシステムが成熟し続け、新しい選択肢が現れることを私は引き続き期待しています。しかしそれまでの間、隣の芝生は青く見えるものですが、無理に乗り換える必要はありません。

The Joy of React

私はこれまで10年近くReactを使用してきましたが、動的なウェブアプリケーションを構築する上で、本当に私のお気に入りの方法であり続けています。数年をかけて、Reactについて知っているすべてをThe Joy of React(別タブで開きます)というインタラクティブな自分のペースで学べるコースにまとめました。

このコースでは、Reactがどのように機能するかのメンタルモデルを構築し、私がReactでこれほど生産的になるのに役立った「幸せな実践(happy practices)」を共有しています。React Server ComponentsやNext.js App Routerについても、SuspenseやStreaming SSRなどの他のモダンな機能と並んで深くカバーしています。

詳細はこちらで学べます:

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。