styled-components を試す
これまで React における CSS 定義に CSS Modules を採用していた。しかし css-loader でサポート廃止が検討されているようだ。また CSS Modules の対抗馬である CSS in JS 系の使用感にも興味があったので代表格とおぼしき styled-components を試すことにした。
例によって実験対象は akabekobeko/examples-electron とする。
環境構築
必要な npm をプロジェクトに追加する。@types/styled-components
は開発時にだけ必要なので -D
にしておく。
$ npm i styled-components
$ npm i -D @types/styled-components typescript-styled-plugin
各 npm の役割は以下。
npm | 役割 |
---|---|
styled-components | 本体。 |
@types/styled-components | TypeScript 型定義。VS Code の入力補完に必要。 |
typescript-styled-plugin | VS Code 上でテンプレート リテラル による CSS 定義の入力補完に必要。 |
styled-components は単体で機能するため webpack 関連のプラグインや設定は不要となる。CSS Modules から移行する際は関連する npm と設定を削除しておくこと。
VS Code の入力補完
VS Code の入力補完を効かせるために tsconfig.json
へ typescript-styled-plugin
を定義する。plugins
内がその部分。
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"jsx": "react",
"declaration": false,
"strict": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"plugins": [{ "name": "typescript-styled-plugin" }]
}
}
次に VS Code の設定。以下のプラグインを追加する。
最後に VS Code の TypeScript バージョンを明示的に指定。
コマンド パレットを開いて Select TypeScript version
を実行すると選択可能な候補が表示されるので、ワークスペースのバージョンを使用する。ワークスペース内に複数プロジェクトを開いている場合は入力補完を効かせたいソースのあるものを選ぶ。ここで VS Code のバージョンや別プロジェクトのものを選ぶと、私の環境では入力補完が動作しなかった。
成功するとテンプレート リテラル内の CSS 定義に対して入力補完の候補が表示されるはず。この機能の有無により開発効率が大きく左右されるため、ぜひ利用したい。
styled-components による CSS 定義
CSS Modules と異なり styled-components は CSS と関連付けられた React コンポーネントを定義する。
import React from 'react'
import styled from 'styled-components'
type Props = {
label: string
onClick: () => void
}
const StyledButton = styled.span`
user-select: none;
cursor: pointer;
display: inline-block;
margin: 1em;
padding: 0.3em 0.5em;
border-radius: 0.2em;
border: solid 1px #3498db;
color: #fff;
background-color: #3498db;
`
export const Button: React.FC<Props> = ({ label, onClick }) => (
<StyledButton onClick={onClick}>{label}</StyledButton>
)
順番に読み解く。
styled
という名前でstyled-components
をimport
styled
は配下にspan
やdiv
などを提供しているので適切なものを選ぶ- コンポーネントに対する CSS 定義はテンプレート リテラルになる、前述の環境構築が成功していれば、ここで CSS としての入力補完が効くはず
- コンポーネントの戻り値を
StyledButton
という変数へ格納 - React コンポーネントとして
props
を指定したうえで公開。
コードの見た目から <span style="...">
になりそうな印象だ。しかし実際のコンポーネント部分は <span class="...">
となりユニークなクラス名を割り当てられ、CSS は HTML の <style>
へまとめて出力される。
CSS ファイルこそ生成しないが使用感は CSS Modules に近い。ただし問題もあって最近の Web ブラウザーだとContent Security Policy に引っかかる。そのため HTML の <meta>
で許可を明示する必要あり。
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'unsafe-inline';"
/>
これを忘れるとエラー扱いでページが表示されないため注意すること。
テーマ
色やレイアウトなどを共通化するための仕組みとして styled-components は ThemeProvider
を提供している。これを利用すれば Object
として定義されたものを下層のコンポーネントに伝搬可能。
テーマはこんな感じ。VS Code で補完を効かせたいので TypeScript としての型も定義しておく。これが Theme.ts
だとして
import 'styled-components'
export const Theme = {
colors: {
text: '#2c3e50'
},
layout: {},
icons: {
addToList: '\\e900'
}
} as const
type AppTheme = typeof Theme
declare module 'styled-components' {
interface DefaultTheme extends AppTheme {}
}
利用側は以下のように指定。
import React from 'react'
import { render } from 'react-dom'
import { ThemeProvider } from 'styled-components'
import { Theme } from './Theme'
// ...中略
window.addEventListener('load', () => {
render(
<ThemeProvider theme={Theme}>
<BasicFunction />
<DialogForm />
</ThemeProvider>,
document.querySelector('.app')
)
})
これで ThemeProvider
に包まれた React コンポーネント BasicFunction
と DialogForm
からテーマを props として参照可能となった。参照はテンプレート リテラル上の関数として定義する。
const StyledDialogForm = styled.div`
padding: 1rem;
text-align: center;
fieldset {
border: solid 1px ${(props) => props.theme.colors.grayDark};
}
`
環境構築が適切なら引数に指定された props
へ VS Code 上で入力補完が効く。テーマは theme
として割り当てられるため props.theme
から前述の colors
や icons
などを参照すると、その結果はテンプレート リテラルに展開される。
ただしテーマを動的に切り替える予定がなければ ThemeProvider
を利用せず Theme
なり MyStyle
のような Object
に外観を定義して直に参照してもよいだろう。実際、入力補完が効くとはいえ関数による参照は記述が回りくどい。
CSS をグローバルに定義する
特定のコンポーネントへ依存せず暗黙的に共有したいもの、例えば @media
や @font-face
などをグローバルに定義する場合は createGlobalStyle
を利用する。
import React from 'react'
import { render } from 'react-dom'
import { createGlobalStyle } from 'styled-components'
const GlobalStyle = createGlobalStyle`
html,
body {
}
.app {
}
@font-face {
}
`
window.addEventListener('load', () => {
render(
<div>
<GlobalStyle />
<BasicFunction />
<DialogForm />
</div>,
document.querySelector('.app')
)
})
createGlobalStyle
は React コンポーネントを返すので、それを全コンポーネントのルートあたりに指定。すると <style>
へそのまま展開される。
アイコン フォントとプロパティー制御
アイコン フォントを利用する場合、まずは前述のように @font-face
をグローバル定義しておく。そのうえでコンポーネントのスタイル定義から font-family
を指定すればよいのだが、アイコンそのものを汎用コンポーネント化したくなるかもしれない。その方法を考えてみた。
export const Icon = styled.i<{ icon: string }>`
font-family: 'icon' !important;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
// Better Font Rendering
-webkit-font-smoothing: antialiased;
&:before {
content: '${(props) => props.icon}';
}
`
styled-components の関数は <{prop: type, ...}>
で任意のプロパティーを取れる。この機能により content
を指定可能とした。利用方法は以下のようになる。
import React from 'react'
import { Icon } from './Icon'
type Props = {
label: string
icon: string
onClick: () => void
}
export const Button: React.FC<Props> = ({ label, icon, onClick }) => (
<button>
<Icon icon={icon} />
{label}
</button>
)
icon
にはフォント内のアイコンを示すユニコード値を指定。例えば \\e900
のようになるだろう。JavaScript 文字列なのでバッククォートは必ずエスケープすること。私は CSS Modules (SCSS) の定義から流用する際にエスケープを忘れ、意味のない指定になり少しハマってしまった。
定義の階層化
CSS で特定クラスの配下になる要素またはクラス階層を定義する例。
.sidebar {
width: 30%
}
.sidebar ul {
}
.sidebar .toolbar {
}
styled-components は SCSS のような記述に対応しているため、これを表現したいなら
const StyledSidebar = styled.div`
width: 30%
ul {
}
.toolbar {
}
`
このように定義できる。出力は xxxx ul {}
や xxxx .toolbar {}
という感じで親コンポーネントのクラス名は自動生成しつつ、子の名前はクラスであっても維持される。そのため親子関係が定義どおりならクラス名を直に指定してもスタイルが効く。
const Sidebar: React.FC<Props> = () => (
<StyledSidebar>
<div className="toolbar">
<Button />
<Button />
</div>
<ul>
<li>Menu 1</li>
<li>Menu 2</li>
</ul>
</StyledSidebar>
)
うまく利用すれば階層化ごとに個別のスタイル用コンポーネントを定義しなくても済む。
まとめ
流石に普及しているだけあって必要な機能はひととおり揃っているし、ツールも含むエコシステムは実用十分だ。惜しいのは typescript-styled-plugin
と Content Security Policy。typescript-styled-plugin
自体の依存は仕方ないとして TypeScript バージョンはワークスペース指定で共通にしたい。Content Security Policy は静的な定義しかないことの明示を条件にしてもよいからファイル出力をサポートする方向で対応してほしいものだ。
CSS in JS はスタイルを JavaScript の Object
で定義する印象があった。これらは似て非なるものなので実際に定義しようとすると疑似要素や複数値の定義などで Object
の制約を避けるため文字列化せざるを得ず、記述と可読の両面でイマイチだ。その回答としてテンプレート リテラルを採用したのは面白い。
半端に Object
でがんばるより、いっそ全体を文字列にする。テンプレート リテラルなら関数として値を解析できるため拡張性も十分だ。入力補完についても JSX/TSX のような JavaScript/TypeScript 拡張するよりもテンプレート リテラルに限定したほうが影響もおさえられそう。そういえば GatsbyJS の GraphQL もテンプレート リテラルを利用していた。
CSS in JS といえば Emotion も気になる。後発だけあって styled-components を踏まえた改善もあるそうなので、機会があれば試したい。