アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

styled-components を試す

November 12, 2020開発styled-components, React

これまで 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.jsontypescript-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 定義に対して入力補完の候補が表示されるはず。この機能の有無により開発効率が大きく左右されるため、ぜひ利用したい。

"ba" に対して入力補完されているところ

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>
)

順番に読み解く。

  1. styled という名前で styled-componentsimport
  2. styled は配下に spandiv などを提供しているので適切なものを選ぶ
  3. コンポーネントに対する CSS 定義はテンプレート リテラルになる、前述の環境構築が成功していれば、ここで CSS としての入力補完が効くはず
  4. コンポーネントの戻り値を StyledButton という変数へ格納
  5. 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 コンポーネント BasicFunctionDialogForm からテーマを 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 から前述の colorsicons などを参照すると、その結果はテンプレート リテラルに展開される。

ただしテーマを動的に切り替える予定がなければ 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 を踏まえた改善もあるそうなので、機会があれば試したい。

Copyright © 2009 - 2020 akabeko.me All Rights Reserved.