この記事の概要
React 19 で変わることのうち、スタイルシートのサポートがあります。
私は以前もこんな実験をしているなど、React でのスタイルの扱われ方に興味があり、いくつか実験をしてみました。
公式に書かれているわけではなく、挙動から判断した内容ではありますが、調べたことを記事にしてみます。
記事を書いている 2024 年 6 月 17 日現在、React 19 は RC 版です。
正式リリース時には挙動が変わっているかもしれませんので、お気をつけください。
内部実装は追えていないため、不備や間違いもあるかもしれません。
もし詳しい方がいたら、ぜひコメントで指摘していただけると幸いです。
React 18 までと 19 との差分
React 18 まで
style要素自体は HTML 要素の 1 つでしか無いので、これまでの React でも扱うことはできました。
ただし、次のように書いたコードが
import { Component1 } from "./Component1";
export default function App() {
  return (
    <>
      <Component1 />
    </>
  );
}
export function Component1() {
  return (
    <>
      <style>
        {`
          .component1 {
            color: red;
          }
        `}
      </style>
      <p className="component1">This is component 1</p>
    </>
  );
};
このように アウトプットされます。
<html lang="en">
  <head>
    <!-- 色々なコード -->
  </head>
  <body>
    <div id="root">
      <style>
        .component1 {
          color: red;
        }
      </style>
      <p class="component1">This is component 1</p>
    </div>
  </body>
</html>
コンポーネントが増えれば
  import { Component1 } from "./Component1";
+ import { Component2 } from "./Component2";
  export default function App() {
    return (
      <>
        <Component1 />
+       <Component2 />
      </>
    );
  }
それだけアウトプットでのstyle要素も増えます。
  <div id="root">
    <style>
      .component1 {
        color: red;
      }
    </style>
    <p class="component1">This is component 1</p>
+   <style>
+     .component2 {
+       color: blue;
+     }
+   </style>
+   <p class="component2">This is component 2</p>
  </div>
使用するのがまったく同じコンポーネントであっても
  import { Component1 } from "./Component1";
- import { Component2 } from "./Component2";
  export default function App() {
    return (
      <>
        <Component1 />
-       <Component2 />
+       <Component1 />
      </>
    );
  }
その都度style要素が挿入されます。
  <div id="root">
    <style>
      .component1 {
        color: red;
      }
    </style>
    <p class="component1">This is component 1</p>
-   <style>
-     .component2 {
-       color: blue;
-     }
-   </style>
-   <p class="component2">This is component 2</p>
+   <style>
+     .component1 {
+       color: red;
+     }
+   </style>
+   <p class="component1">This is component 1</p>
  </div>
これが React 18 までのstyleの挙動です。
React 19
上記のコードとまったく同じものであれば挙動は変わりません。
React 19 で新たに加わった機能を適用するためにstyleにhrefとprecedenceという props を渡します。
hrefは props を受け取る場合それらをすべて含んだような名前が良いです(理由は後述します)。
precedence は style を挿入する順番を制御するようなのですが、どういう条件で動きが変わるのかが分かりませんでした……。
この記事ではすべて"medium"に統一してあります。
  export function Component1() {
    return (
      <>
-       <style>
+       <style href="component1" precedence="medium">
          {`
            .component1 {
              color: red;
            }
          `}
        </style>
        <p className="component1">This is component 1</p>
      </>
    );
  };
すると、アウトプットではstyleがheadの中に移動しました。
<html lang="en">
  <head>
    <!-- 色々なコード -->
+   <style data-href="component1" data-precedence="medium">
+     .component1 {
+       color: red;
+     }
+   </style>
  </head>
  <body>
    <div id="root">
      <p class="component1">This is component 1</p>
    </div>
  </body>
</html>
また、同じコンポーネントをもう 1 つ使用してみます。
  import { Component1 } from "./Component1";
  export default function App() {
    return (
      <>
        <Component1 />
+       <Component1 />
      </>
    );
  }
重複しているstyleは 1 つにまとめられています。
<html lang="en">
  <head>
    <!-- 色々なコード -->
    <style data-href="component1" data-precedence="medium">
      .component1 {
        color: red;
      }
    </style>
  </head>
  <body>
    <div id="root">
      <p class="component1">This is component 1</p>
+     <p class="component1">This is component 1</p>
    </div>
  </body>
</html>
このように、今までは「書いてある通りの位置」に出力されていたstyleが、headの中に移動し、重複もなくなるようにアップデートされています。
色々なパターンの実験
ここからが本題です。
概念的には理解できましたが、どういうパターンでどういう動きになるのか、細かいことが分かっていません。
色々試してみます。
props によってスタイルが変わる場合
props を受け取って、その色に合わせて変わるようにしてみました。
- export function Component1() {
+ export function Component1({ color }) {
    return (
      <>
        <style href="component1" precedence="medium">
          {`
-           .component1 {
-             color: red;
+           .component1-${color} {
+             color: ${color};
            }
          `}
        </style>
-       <p className="component1">This is component 1</p>
+       <p className={`component1-${color}`}>This is component 1</p>
      </>
    );
  };
redとblueを指定します。
  import { Component1 } from "./Component1";
  export default function App() {
    return (
      <>
-       <Component1 />
-       <Component1 />
+       <Component1 color="red" />
+       <Component1 color="blue" />
      </>
    );
  }
このようにアウトプットされました。
<html lang="en">
  <head>
    <!-- 色々なコード -->
    <style data-href="component1" data-precedence="medium">
-     .component1 {
+     .component1-red {
        color: red;
      }
    </style>
  </head>
  <body>
    <div id="root">
-     <p class="component1">This is component 1</p>
-     <p class="component1">This is component 1</p>
+     <p class="component1-red">This is component 1</p>
+     <p class="component1-blue">This is component 1</p>
    </div>
  </body>
</html>
pにつくクラスはcomponent1-redとcomponent1-blueが出力されましたが、styleにはcomponent1-redしか出力されていません。
なお、redとblueを指定する順番を変えると
  import { Component1 } from "./Component1";
  export default function App() {
    return (
      <>
+       <Component1 color="blue" />
+       <Component1 color="red" />
      </>
    );
  }
blueのスタイルだけが出力されます。
<html lang="en">
  <head>
    <!-- 色々なコード -->
    <style data-href="component1" data-precedence="medium">
-     .component1 {
+     .component1-blue {
+       color: blue;
      }
    </style>
  </head>
  <body>
    <div id="root">
-     <p class="component1">This is component 1</p>
-     <p class="component1">This is component 1</p>
+     <p class="component1-blue">This is component 1</p>
+     <p class="component1-red">This is component 1</p>
    </div>
  </body>
</html>
props によってスタイルが変わり、href がユニークな場合
ドキュメントには以下の記載があります。1
The href prop should uniquely identify the stylesheet, because React will de-duplicate stylesheets that have the same href.
今回でいうとcomponent1-redもcomponent1-blueも同じ href を持っており、重複として排除されてしまったと考えます。
redとblueの順番を変えたら生成されるスタイルが変わったことからも、おそらくそうだと思います。2
というわけでhrefを書き換えます。
  export function Component1({ color }) {
    return (
      <>
-       <style href="component1" precedence="medium">
+       <style href={`component1-${color}`} precedence="medium">
          {`
            .component1-${color} {
              color: ${color};
            }
          `}
        </style>
        <p className={`component1-${color}`}>This is component 1</p>
      </>
    );
  };
すると出力はこのように変わりました。
<html lang="en">
  <head>
    <!-- 色々なコード -->
-   <style data-href="component1" data-precedence="medium">
+   <style data-href="component1-red" data-precedence="medium">
      .component1-red {
        color: red;
      }
    </style>
+   <style data-href="component1-blue" data-precedence="medium">
+     .component1-blue {
+       color: blue;
+     }
+   </style>
  </head>
  <body>
    <div id="root">
      <p class="component1-red">This is component 1</p>
      <p class="component1-blue">This is component 1</p>
    </div>
  </body>
</html>
無事にredとblue両方のスタイルを出力することができました。
href がユニークなコンポーネントを 1 つしか使わない場合
コンポーネントの種類にあわせてスタイルが両方出力されたのは嬉しいですが、どれか 1 つしか使っていないのにすべてのスタイルが出力されていたら邪魔そうです。
確かめてみます。
  import { Component1 } from "./Component1";
  export default function App() {
    return (
      <>
-       <Component1 color="red" />
+       <Component1 color="blue" />
      </>
    );
  }
出力結果は以下です。
<html lang="en">
  <head>
    <!-- 色々なコード -->
-   <style data-href="component1-red" data-precedence="medium">
-     .component1-red {
-       color: red;
-     }
-   </style>
    <style data-href="component1-blue" data-precedence="medium">
      .component1-blue {
        color: blue;
      }
    </style>
  </head>
  <body>
    <div id="root">
-     <p class="component1-red">This is component 1</p>
      <p class="component1-blue">This is component 1</p>
    </div>
  </body>
</html>
問題なく、使用しているスタイルだけが出力されました。
props で変化するスタイルと変化しないスタイルを両方持っている場合
colorはpropsにあわせて変わるけどfont-sizeはいつでも同じとします。
  export function Component1({ color }) {
    return (
      <>
        <style href={`component1-${color}`} precedence="medium">
          {`
            .component1-${color} {
              color: ${color};
+             font-size: 1.5rem;
+             padding: 0.5rem;
            }
          `}
        </style>
        <p className={`component1-${color}`}>This is component 1</p>
      </>
    );
  };
出力結果は以下です。
<html lang="en">
  <head>
    <!-- 色々なコード -->
    <style data-href="component1-red" data-precedence="medium">
      .component1-red {
        color: red;
+       font-size: 1.5rem;
+       padding: 0.5rem;
      }
    </style>
    <style data-href="component1-blue" data-precedence="medium">
      .component1-blue {
        color: blue;
+       font-size: 1.5rem;
+       padding: 0.5rem;
      }
    </style>
  </head>
  <body>
    <div id="root">
      <p class="component1-red">This is component 1</p>
      <p class="component1-blue">This is component 1</p>
    </div>
  </body>
</html>
それぞれのクラスが出力されてしまいました。
今は 1 行だけだから問題でもありませんが、共通部分が多い場合は少し嫌ですね。
この場合、共通部分だけ切り出した方が良さそうです。
hrefにあわせて出力されるので、このように変えます。
  export function Component1({ color }) {
    return (
      <>
+       <style href="component1" precedence="medium">
+         {`
+           .component1 {
+             font-size: 1.5rem;
+             padding: 0.5rem;
+           }
+         `}
+       </style>
        <style href={`component1-${color}`} precedence="medium">
          {`
            .component1-${color} {
              color: ${color};
-             font-size: 1.5rem;
-             padding: 0.5rem;
            }
          `}
        </style>
-       <p className={`component1-${color}`}>This is component 1</p>
+       <p className={`component1 component1-${color}`}>This is component 1</p>
      </>
    );
  };
このように出力されました。
<html lang="en">
  <head>
    <!-- 色々なコード -->
+   <style data-href="component1" data-precedence="medium">
+     .component1 {
+       font-size: 1.5rem;
+       padding: 0.5rem;
+     }
+   </style>
    <style data-href="component1-red" data-precedence="medium">
      .component1-red {
        color: red;
-       font-size: 1.5rem;
-       padding: 0.5rem;
      }
    </style>
    <style data-href="component1-blue" data-precedence="medium">
      .component1-blue {
        color: blue;
-       font-size: 1.5rem;
-       padding: 0.5rem;
      }
    </style>
  </head>
  <body>
    <div id="root">
      <p class="component1 component1-red">This is component 1</p>
      <p class="component1 component1-blue">This is component 1</p>
    </div>
  </body>
</html>
これなら、コードが増えるのは変化がある部分だけで済みます。
まったく同じスタイルを持つ別のコンポーネントが存在した場合
別のコンポーネントですが、使っているスタイルが共通のものがあったとします。
ここでは、色がredとblueで共通です。
export function Component2({ color }) {
  return (
    <>
      <style href={`component2-${color}`} precedence="medium">
        {`
          .component2-${color} {
            color: ${color};
          }
        `}
      </style>
      <p className={`component2-${color}`}>This is component 2</p>
    </>
  );
};
新たなコンポーネントを使用します。
  import { Component1 } from "./Component1";
+ import { Component2 } from "./Component2";
  export default function App() {
    return (
      <>
        <Component1 color="red" />
        <Component1 color="blue" />
+       <Component2 color="red" />
+       <Component2 color="blue" />
      </>
    );
  }
アウトプットはこのようになりました。
<html lang="en">
  <head>
    <!-- 色々なコード -->
    <style data-href="component1" data-precedence="medium">
      [class^="component1-"] {
        font-size: 1.5rem;
        padding: 0.5rem;
      }
    </style>
    <style data-href="component1-red" data-precedence="medium">
      .component1-red {
        color: red;
      }
    </style>
    <style data-href="component1-blue" data-precedence="medium">
      .component1-blue {
        color: blue;
      }
    </style>
+   <style data-href="component2-red" data-precedence="medium">
+     .component2-red {
+       color: red;
+     }
+   </style>
+   <style data-href="component2-blue" data-precedence="medium">
+     .component2-blue {
+       color: blue;
+     }
    </style>
  </head>
  <body>
    <div id="root">
      <p class="component1 component1-red">This is component 1</p>
      <p class="component1 component1-blue">This is component 1</p>
+     <p class="component2-red">This is component 2</p>
+     <p class="component2-blue">This is component 2</p>
    </div>
  </body>
</html>
あくまで重複削除のトリガーはhrefのようです。
StyleX のようにatomic CSS3として整理してくれるわけではないみたいです。
仮に両方のコンポーネントでクラス名を揃えても、重複は残ったままです。
複数のpropsを持つ場合
色だけでなく、フォントサイズも調整できるコンポーネントに変えます。
- export function Component2({ color }) {
+ export function Component2({ color, size }) {
    return (
      <>
-       <style href={`component2-${color}`} precedence="medium">
+       <style href={`component2-${color}-${size}`} precedence="medium">
          {`
-           .component2-${color} {
+           .component2-${color}-${size} {
              color: ${color};
+             font-size: ${size}px;
            }
          `}
        </style>
-       <p className={`component2-${color}`}>This is component 2</p>
+       <p className={`component2-${color}-${size}`}>This is component 2</p>
      </>
    );
  };
色々なパターンを呼び出します。
  import { Component1 } from "./Component1";
  import { Component2 } from "./Component2";
  export default function App() {
    return (
      <>
        <Component1 color="red" />
        <Component1 color="blue" />
-       <Component2 color="red" />
-       <Component2 color="blue" />
+       <Component2 color="red" size={10} />
+       <Component2 color="red" size={20} />
+       <Component2 color="blue" size={30} />
+       <Component2 color="blue" size={40} />
      </>
    );
  }
このように出力されました。
<html lang="en">
  <head>
    <!-- 色々なコード -->
    <style data-href="component1" data-precedence="medium">...</style>
    <style data-href="component1-red" data-precedence="medium">...</style>
    <style data-href="component1-blue" data-precedence="medium">...</style>
-   <style data-href="component2-red" data-precedence="medium">
-     .component2-red {
-       color: red;
-     }
-   </style>
-   <style data-href="component2-blue" data-precedence="medium">
-     .component2-blue {
-       color: blue;
-     }
-   </style>
+   <style data-href="component2-red-10" data-precedence="medium">
+     .component2-red-10 {
+       color: red;
+       font-size: 10px;
+     }
+   </style>
+   <style data-href="component2-red-20" data-precedence="medium">
+     .component2-red-20 {
+       color: red;
+       font-size: 20px;
+     }
+   </style>
+   <style data-href="component2-blue-30" data-precedence="medium">
+     .component2-blue-30 {
+       color: blue;
+       font-size: 30px;
+     }
+   </style>
+   <style data-href="component2-blue-40" data-precedence="medium">
+     .component2-blue-40 {
+       color: blue;
+       font-size: 40px;
+     }
+   </style>
  </head>
  <body>
    <div id="root">
      <p class="component1 component1-red">This is component 1</p>
      <p class="component1 component1-blue">This is component 1</p>
-     <p class="component2-red">This is component 2</p>
-     <p class="component2-blue">This is component 2</p>
+     <p class="component2-red-10">This is component 2</p>
+     <p class="component2-red-20">This is component 2</p>
+     <p class="component2-blue-30">This is component 2</p>
+     <p class="component2-blue-40">This is component 2</p>
    </div>
  </body>
</html>
ここでもあくまでhrefによる出し分けなので、分離した方が少しだけ重複を減らせます。4
  export function Component2({ color, size }) {
    return (
      <>
-       <style href={`component2-${color}-${size}`} precedence="medium">
+       <style href={`component2-${color}`} precedence="medium">
          {`
-           .component2-${color}-${size} {
+           .component2-${color} {
              color: ${color};
-             font-size: ${size}px;
            }
          `}
        </style>
+       <style href={`component2-${size}`} precedence="medium">
+         {`
+           .component2-${size} {
+             font-size: ${size}px;
+           }
+         `}
+       </style>
-       <p className={`component2-${color}-${size}`}>This is component 2</p>
+       <p className={`component2-${color} component2-${size}`}>This is component 2</p>
      </>
    );
  };
<html lang="en">
  <head>
    <!-- 色々なコード -->
    <style data-href="component1" data-precedence="medium">...</style>
    <style data-href="component1-red" data-precedence="medium">...</style>
    <style data-href="component1-blue" data-precedence="medium">...</style>
-   <style data-href="component2-red-10" data-precedence="medium">
-     .component2-red {
-       color: red;
-       font-size: 10px;
-     }
-   </style>
-   <style data-href="component2-red-20" data-precedence="medium">
-     .component2-red {
-       color: red;
-       font-size: 20px;
-     }
-   </style>
-   <style data-href="component2-blue-30" data-precedence="medium">
-     .component2-blue {
-       color: blue;
-       font-size: 30px;
-     }
-   </style>
-   <style data-href="component2-blue-40" data-precedence="medium">
-     .component2-blue {
-       color: blue;
-       font-size: 40px;
-     }
-   </style>
+   <style data-href="component2-red" data-precedence="medium">
+     .component2-red {
+       color: red;
+     }
+   </style>
+   <style data-href="component2-blue" data-precedence="medium">
+     .component2-blue {
+       color: blue;
+     }
+   </style> 
+   <style data-href="component2-10" data-precedence="medium">
+     .component2-10 {
+       font-size: 10px;
+     }
+   </style>
+   <style data-href="component2-20" data-precedence="medium">
+     .component2-20 {
+       font-size: 20px;
+     }
+   </style>
+   <style data-href="component2-30" data-precedence="medium">
+     .component2-30 {
+       font-size: 30px;
+     }
+   </style>
+   <style data-href="component2-40" data-precedence="medium">
+     .component2-40 {
+       font-size: 40px;
+     }
+   </style>
  </head>
  <body>
    <div id="root">
      <p class="component1 component1-red">This is component 1</p>
      <p class="component1 component1-blue">This is component 1</p>
-     <p class="component2-red-10">This is component 2</p>
-     <p class="component2-red-20">This is component 2</p>
-     <p class="component2-blue-30">This is component 2</p>
-     <p class="component2-blue-40">This is component 2</p>
+     <p class="component2-red component2-10">This is component 2</p>
+     <p class="component2-red component2-20">This is component 2</p>
+     <p class="component2-blue component2-30">This is component 2</p>
+     <p class="component2-blue component2-40">This is component 2</p>
    </div>
  </body>
</html>
今回調べたことから分かることのまとめ
- 
hrefとprecedenceを忘れない
- 
hrefを見て重複スタイルかどうかが判断される- 
styleの中身は判定には使われていない
- 
propsごとに 1 つのhrefを指定した方が良さそう
- 共通部分も 1 つhrefを用意した方が良さそう
 
- 
ただし記事冒頭にも書いた通り、試したパターンから予測しているに過ぎないのと、RC 版なので正式版では変わっている可能性があります。
改めてご了承ください。
- 
https://react.dev/reference/react-dom/components/style#rendering-an-inline-css-stylesheet ↩ 
- 
実装を見れば「考えます」とか「思います」じゃなく、事実を書けそうなものですが、私の実力では分かりませんでした。 ↩ 
- 
今は実験用のコードでスタイルの行数が少なく、間に入る </style><style>の行のが多いですが、実際はスタイルの指定の方が遥かに多いから効率的になる……はず。 ↩
