😺
コンポーネントベースで開発する時の CSS の書き方とコンポーネントの分類 (自己流)
- React や Svelte でコンポーネントベースで開発するとき特有の CSS ノウハウってあんまり効かない気がする
- Twitter に書いたら反響があったので、自己流だけどまとめておく
- React Component の管理単位と、CSS としてのレイアウトの管理ポリシーは違うよね、みたいな話をマークアップエンジニアに時折されるが、そんな話は無視して完全一致させる。そういう星のもとで開発している
コンポーネントの分類
- ロジックコンポーネント
- レイアウトコンポーネント
- ブロックコンポーネント
- インラインコンポーネント
定義
- ロジックコンポーネント
- Provider や hooks などのデータ処理だけを扱い、子に渡すコンポーネント
- 一切の CSS や DOM 実体を持たない
- レイアウトコンポーネント
- レイアウトコンポーネントは複数の子ブロックコンポーネント(または slot)を持ち、子ブロックコンポーネントを配置を決定することのみを責務とする
- ルート以外のレイアウトコンポーネントは基本的に
width: 100%, height: 100%; margin: 0; padding: 0; box-sizing: border-box;
でdisplay: grid
ordisplay: flex
。 grid 推奨 - レイアウトコンポーネントは自身の占める領域を
flex
,grid
などを用いて領域を分割する - 分割された領域に対して、配置可能なブロックコンポーネントの数は 1 または 0 である
- レスポンシブの実装もここで行う
- ブロックコンポーネント
- ブロックコンポーネントはレイアウトから与えられた領域を埋める要素
- ブロックコンポーネントのルート要素は
width: 100%; height: 100%; margin:0; padding: 0; box-sizing: border-box; display: flex;
- ブロックコンポーネントの内部は、 1つのレイアウトコンポーネント、 または複数のインラインコンポーネントで構成される
- インラインコンポーネント
- 高さや幅を持たず、それ自身のコンテンツでサイズが決まる要素
- width, height が指定された固定サイズのコンテナー要素を挟んだブロックコンポーネントまたはレイアウトコンポーネントは、インラインコンポーネントとして扱う
- 例外的な扱い
- 経験上、 DnD のようなデータと DOM構造が密な場合、ロジックコンポーネントとそれ以外を分離するのは不可能または困難
- サービスがアプリケーション的なレイアウトの場合、レイアウトコンポーネントのルート要素は
width: 100vw; height: 100vh;
とすることが多いが、伝統的な縦に伸びるサービスでは、 ルートから近いレイアウトの一部ではheight: auto; overflow: auto;
とする。
結果として Root => Layout => Block => Inline or Layout => Block => ... のような Layout と Block の繰り返しになる
サンプル
- 説明用に style に直書きしている。(何かのCSSフレームワークを前提にするとその説明が必要なので...)
- 説明用に適当に書いたので、コードとしての可読性を重視しているが、実際何がレンダリングされるかみてない
// Logic Component
const DataContext = createContext<{ value: number }>({ value: 0 });
function RootProvider(props: {children: React.ReactElement}) {
return <DataContext.Provider value={{ value: 1 }}>
{props.children}
</DataContext.Provider>;
}
// Layout Component
function RootLayout(props: {
header: React.ReactElement;
content: React.ReactElement;
footer: React.ReactElement;
}) {
return <div style={{
width: "100vw",
height: "100vh",
overflow: "hidden",
display: "grid",
gridTemplateAreas: `
'header'
'content'
'footer'
`,
gridTemplateColumns: "1fr",
gridTemplateRows: "100px 1fr 300px"
}}>
<div style={{gridArea: "header"}}>{props.header}</div>
<div style={{gridArea: "content"}}>{props.content}</div>
<div style={{gridArea: "content"}}>{props.footer}</div>
</div>
}
// Inline Component
function Title() {
return <div>Hello, Example</div>;
}
// Block Component
function HeaderBlock() {
return <div style={{
width: "100%",
height: "100%",
margin: "0",
padding: "0",
display: "flex",
}}>
Header
</div>;
}
// Block Component
function ContentBlock() {
return <div style={{
width: "100%",
height: "100%",
margin: "0",
padding: "0",
display: "flex",
}}>
<Title />
Content~~~
</div>;
}
// Block Component
function FooterBlock() {
return <div style={{
width: "100%",
height: "100%",
margin: "0",
padding: "0",
display: "flex",
}}>
Footer
</div>;
}
// Root Component
function App() {
return <RootProvider>
<RootLayout
header={<HeaderBlock />}
content={<ContentBlock />}
footer={<FooterBlock />}
/>
</RootProvider>
}
メリット
- CSS に何の属性を書いたかで、コンポーネントの種類が自然と分類される
- ブロックコンポーネントは常に
width: 100%, height; 100%
として扱うので、どのブロックも交換可能 - レイアウトとブロックに分離することで、要素の配置変更に強く、レスポンシブや複数の画面表示モードの実装も容易
- ほとんどの要素に対して高さが事前に確定するので、スケルトンスクリーンの実装やクリティカルレンダリングパス最適化に優しい
デメリット
- リキッドレイアウトに対して一貫した height 継承ポリシーを持ちづらい
- 油断するとブロックコンポーネントが overflow しやすい
- 慣習的な(Web制作の)スタイルとは言い難い
Discussion
記事、読みました!
記事の冒頭にある「コンポーネントの種類リスト」が興味深かったです。
この4つは、mizchiさん流の分類方法なのでしょうか。「自分ではこういう名称で分けている」がまとまっている方は、一緒に仕事をする際にありがたいですね。
CSSデザインについては、私もWebアプリ等を趣味レベルで作るのですが「とにかくまずはgridレイアウト」で色々なんとかなる、と感じていますw 具体的にはCSSファイルの先頭に * {display: grid;} 書いてるくらいw