まとまったCSSを別のコンポーネントに分けないでほしい話
2020年10月15日 公開
この記事は、ReactでCSSを書くときに関連したCSSを別々のコンポーネントに分けるのをやめようという記事です。主な理由は、スタイリングという機能が複数コンポーネントに分散するのを防ぐためです。これには修正時に複数コンポーネントにまたがって修正が必要になるのを防ぐという意味もあります。
Flexboxの例
関連したCSSが複数の要素に分かれることはよくあります。その代表例がdisplay: flex
です。例えばこんなレイアウトを考えてみましょう。左側のボックスの幅が決まっていて右側の幅が可変の2カラムレイアウトです。
このレイアウトはおおよそ次のように実現できます。
/* 親要素 */
display: flex;
/* 子要素(左) */
flex: 100px 0 0;
/* 子要素(右) */
flex: auto 1 0;
では、Reactではどのように書くでしょうか。CSS in JSの方法は何でもいいのですが、ここではstyled-componentsを用いて書いてみます。
だめな例
まず、やりがちですがだめな例です。
const Wrapper = styled(({ className }) =>
<div className={className}>
<LeftColumn />
<RightColumn />
</div>
)`
display: flex;
`
const LeftColumn = styled(({ className }) =>
<div className={className}>
左のカラム (100px)
</div>
)`
flex: 100px 0 0;
padding: 10px;
`
const RightColumn = styled(({ className }) =>
<div className={className}>
右のカラム
</div>
)`
flex: auto 1 0;
padding: 10px;
`
なぜだめなのかと言えば、2カラムレイアウトを実現するために必要なCSSが複数のコンポーネントに分散しているからです。
これだとスタイルの全体像を把握するために3つのコンポーネントを読む必要があり、修正時、例えばdisplay: flex
をやめてdisplay: grid
にしたい場合、3つのコンポーネント全てを編集する必要があります。
良い例
この場合、次のようにすべきです。
const Wrapper = styled(({ className }) =>
<div className={className}>
<div>
<LeftColumn />
</div>
<div>
<RightColumn />
</div>
</div>
)`
display: flex;
& > div:first-child {
flex: 100px 0 0;
}
& > div:last-child {
flex: auto 1 0;
}
`
const LeftColumn = styled(({ className }) =>
<div className={className}>
左のカラム (100px)
</div>
)`
padding: 10px;
`
const RightColumn = styled(({ className }) =>
<div className={className}>
右のカラム
</div>
)`
padding: 10px;
`
これならば、2カラムレイアウトに必要なCSSは全てWrapper
に属しており、LeftColumn
やRightColumn
はカラムの中身に集中しています。責務の観点からも、2カラムレイアウトを作るという責務がWrapper
に集中しており、このようにCSSを分割する方が適切です。さらに言えば、Wrapper
から直接LeftColumn
やRightColumn
を参照するのではなく、Wrapper
をLayout
みたいな名前にして、子コンテンツはLayout
を使う側から挿入するのも良いでしょう。
良くない例
ところで、元々の例に比べると「良い例」ではdiv
の層が一つ増えていますね。次のようにするのはだめなのかと思われるかもしれません。
const Wrapper = styled(({ className }) =>
<div className={className}>
<LeftColumn />
<RightColumn />
</div>
)`
display: flex;
& > div:first-child {
flex: 100px 0 0;
}
& > div:last-child {
flex: auto 1 0;
}
`
しかし、これは悪手です。なぜなら、これは、LeftColumn
やRightColumn
がdiv
をレンダリングするということに依存しているからです。すなわち、Wrapper
の実装がLeftColumn
やRightColumn
の内部実装に依存している形になり、好ましくありません。
また、& > *:first-child { ... }
のようにするのも同様に避けるべきです。これはLeftColumn
などがdiv
ではなく何をレンダリングしても対応できるように思えますが、LeftColumn
などが要素を一つだけレンダリングするということに依存してしまっています(Fragment
を用いれば一つのコンポーネントが複数の要素をレンダリングすることができます)。
他にも、記事を公開すると「親から子にclassNameを渡す」などの形で、親の側から子のスタイルを指定すれば良いのではないかという意見もありました。styled-componentsで書くとこんな感じです。
const Wrapper = styled(({ className }) =>
<div className={className}>
<LeftColumn />
<RightColumn />
</div>
)`
display: flex;
& > ${LeftColumn} {
flex: 100px 0 0;
}
& > ${RightColumn} {
flex: auto 1 0;
}
`
つまり、LeftColumn
やRightColumn
(の一番外の要素)をWrapper
から指定するようにします。これならWrapper
にスタイルが集中しているように見えます。
このやり方は最初のだめな例に比べれば幾分ましですが、やはりお勧めしません。Wrapper
からLeftColumn
やRightColumn
の内部実装への依存が残っているからです。
つまり、こうするとレンダリングされたLeftColumn
の中には、Wrapper
由来のスタイルとLeftColumn
由来のスタイルが混在することになります。この状態では、LeftColumn
側のスタイルを変更すると、それとWrapper
由来のスタイルがうまく噛み合わずに変な描画結果になってしまう恐れがあります。
これではLeftColumn
がうかつに自身のスタイルを変更できないことになり、コンポーネントのインターフェース設計に失敗しています。外部から自身のスタイルを変更できるようなコンポーネントのインターフェースはアンチパターンと言うべきでしょう。
これらの諸問題を避けるためには、スタイリングが自身の中で完結して他に干渉しないコンポーネントと作るのが望ましいでしょう。
まとめ
ReactでCSSを書くときはコンポーネント間の依存を発生させないように書きましょう。
余談: react-wcの宣伝
上の例を見て、> div:first-child
というのが物々しくて気に入らないと思った方が居るかもしれません。実は、筆者が最近作ったreact-wcならこの点を解決して次のようにクラスを使って書けます。Shadow DOMを使っているのでleft
やright
といったクラス名が他とかぶることは心配する必要がありません。いいねと思った方は試してみてください。
const Wrapper = html`
<style>
:host {
display: flex;
}
.left {
flex: 100px 0 0;
}
.right {
flex: auto 1 0;
}
</style>
<div class="left">${slot("left")}</div>
<div class="right">${slot("right")}</div>
`;
<Wrapper
left={<LeftColumn />}
right={<RightColumn />}
/>