この記事の概要
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>
の行のが多いですが、実際はスタイルの指定の方が遥かに多いから効率的になる……はず。 ↩