React 19 での style 要素の実験

## この記事の概要

React 19 で変わることのうち、[スタイルシートのサポート](https://react.dev/blog/2024/04/25/react-19#support-for-stylesheets)があります。

私は以前も[こんな実験](https://qiita.com/xrxoxcxox/items/e24ec739db993bd41ec1)をしているなど、React でのスタイルの扱われ方に興味があり、いくつか実験をしてみました。

公式に書かれているわけではなく、挙動から判断した内容ではありますが、調べたことを記事にしてみます。

:::note
記事を書いている 2024 年 6 月 17 日現在、React 19 は RC 版です。
正式リリース時には挙動が変わっているかもしれませんので、お気をつけください。
:::

:::note warn
内部実装は追えていないため、不備や間違いもあるかもしれません。
もし詳しい方がいたら、ぜひコメントで指摘していただけると幸いです。
:::

## React 18 までと 19 との差分

### React 18 まで

`style`要素自体は HTML 要素の 1 つでしか無いので、これまでの React でも扱うことはできました。

ただし、次のように書いたコードが

```jsx:App.jsx
import { Component1 } from "./Component1";

export default function App() {
  return (
    <>
      <Component1 />
    </>
  );
}
```

```jsx:Component1.jsx
export function Component1() {
  return (
    <>
      <style>
        {`
          .component1 {
            color: red;
          }
        `}
      </style>
      <p className="component1">This is component 1</p>
    </>
  );
};
```

このように アウトプットされます。

```html
<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>
```

---

コンポーネントが増えれば

```diff_jsx:App.jsx
  import { Component1 } from "./Component1";
+ import { Component2 } from "./Component2";

  export default function App() {
    return (
      <>
        <Component1 />
+       <Component2 />
      </>
    );
  }
```

それだけアウトプットでの`style`要素も増えます。

```diff_html
  <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>
```

---

使用するのがまったく同じコンポーネントであっても

```diff_jsx:App.jsx
  import { Component1 } from "./Component1";
- import { Component2 } from "./Component2";

  export default function App() {
    return (
      <>
        <Component1 />
-       <Component2 />
+       <Component1 />
      </>
    );
  }
```

その都度`style`要素が挿入されます。

```diff_html
  <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"`に統一してあります。

```diff_jsx:Component1.jsx
  export function Component1() {
    return (
      <>
-       <style>
+       <style href="component1" precedence="medium">
          {`
            .component1 {
              color: red;
            }
          `}
        </style>
        <p className="component1">This is component 1</p>
      </>
    );
  };
```

すると、アウトプットでは`style`が`head`の中に移動しました。

```diff_html
<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 つ使用してみます。

```diff_jsx:App.jsx
  import { Component1 } from "./Component1";

  export default function App() {
    return (
      <>
        <Component1 />
+       <Component1 />
      </>
    );
  }
```

重複している`style`は 1 つにまとめられています。

```diff_html
<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 を受け取って、その色に合わせて変わるようにしてみました。

```diff_jsx:Component1.jsx
- 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`を指定します。

```diff_jsx:App.jsx
  import { Component1 } from "./Component1";

  export default function App() {
    return (
      <>
-       <Component1 />
-       <Component1 />
+       <Component1 color="red" />
+       <Component1 color="blue" />
      </>
    );
  }
```

このようにアウトプットされました。

```diff_html
<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`を指定する順番を変えると

```diff_jsx:App.jsx
  import { Component1 } from "./Component1";

  export default function App() {
    return (
      <>
+       <Component1 color="blue" />
+       <Component1 color="red" />
      </>
    );
  }
```

`blue`のスタイルだけが出力されます。

```diff_html
<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 がユニークな場合

ドキュメントには以下の記載があります。[^unique-href-prop]

[^unique-href-prop]: https://react.dev/reference/react-dom/components/style#rendering-an-inline-css-stylesheet

> 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`の順番を変えたら生成されるスタイルが変わったことからも、おそらくそうだと思います。[^i-couldnt-decipher-the-code]

[^i-couldnt-decipher-the-code]: 実装を見れば「考えます」とか「思います」じゃなく、事実を書けそうなものですが、私の実力では分かりませんでした。

というわけで`href`を書き換えます。

```diff_jsx:Component1.jsx
  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>
      </>
    );
  };
```

すると出力はこのように変わりました。

```diff_html
<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 つしか使っていないのにすべてのスタイルが出力されていたら邪魔そうです。

確かめてみます。

```diff_jsx:App.jsx
  import { Component1 } from "./Component1";

  export default function App() {
    return (
      <>
-       <Component1 color="red" />
+       <Component1 color="blue" />
      </>
    );
  }
```

出力結果は以下です。

```diff_html
<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`はいつでも同じとします。

```diff_jsx:Component1.jsx
  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>
      </>
    );
  };
```

出力結果は以下です。

```diff_html
<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`にあわせて出力されるので、このように変えます。

```diff_jsx:Component1.jsx
  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>
      </>
    );
  };
```

このように出力されました。

```diff_html
<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`で共通です。

```jsx:Component2.jsx
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>
    </>
  );
};
```

新たなコンポーネントを使用します。

```diff_jsx:App.jsx
  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" />
      </>
    );
  }
```

アウトプットはこのようになりました。

```diff_html
<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](https://stylexjs.com/) のようにatomic CSS[^atomic-css]として整理してくれるわけではないみたいです。
仮に両方のコンポーネントでクラス名を揃えても、重複は残ったままです。

[^atomic-css]: https://stylexjs.com/blog/introducing-stylex

### 複数の`props`を持つ場合

色だけでなく、フォントサイズも調整できるコンポーネントに変えます。

```diff_jsx:Component2.jsx
- 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>
      </>
    );
  };
```

色々なパターンを呼び出します。

```diff_jsx:App.jsx
  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} />
      </>
    );
  }
```

このように出力されました。

```diff_html
<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`による出し分けなので、分離した方が少しだけ重複を減らせます。[^remove-duplicate-codes]

[^remove-duplicate-codes]: 今は実験用のコードでスタイルの行数が少なく、間に入る`</style><style>`の行のが多いですが、実際はスタイルの指定の方が遥かに多いから効率的になる……はず。

```diff_jsx:Component2.jsx
  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>
      </>
    );
  };
```

```diff_html
<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`を用意した方が良さそう

:::note warn
ただし記事冒頭にも書いた通り、試したパターンから予測しているに過ぎないのと、RC 版なので正式版では変わっている可能性があります。
改めてご了承ください。
:::