reactjs
CSS設計
styled-components

styled-componentsを使ったCSS設計

この記事はRecruit Engineers Advent Calendar 2017の記事です。

はじめに

自分は普段フロントエンドエンジニアとして、React Reduxなアーキテクチャのアプリを作ることが多いのですが、stylingにstyled-componentsを導入しています。今回は、styled-componentsでのCSS設計について書いてみたいと思います。

styled-componentsとは

JSでstyleを記述するCSS in JSのライブラリで、2017年12月現在最も人気のあるライブラリです。タグ付きテンプレートリテラルをうまく使った独自性と、明快なAPIでCSS in JSの火付け役にもなり、同じ思想を持った亜種ライブラリ(paypal/glamorous, zeit/styled-jsx等)が続々と出て来るなどある種のブームを巻き起こしました。

そもそも、Reactの登場でフロントエンド開発が「jQueryによるhtmlへの振る舞いの後付け」というスクレイピング的手法から、「構造と振る舞い(状態)を持ったUIの構築」というUIエンジニアリング的手法へとシフトする中、CSSだけが依然として「classNameへのマッピングによるスタイルの後付け」というスクレイピング的手法のままになっていました。styled-componentsはそこに焦点を当て、Reactでのフロントエンド開発を「構造と振る舞い(状態)、スタイルを持ったUIの構築」へと昇華させることに成功しました。

ここでは詳しいAPIについては割愛しますので、公式ページを参照ください。

styled-componentsで変わること

styled-componentsを使うことで、Reactのinline stylingやsassでのstylingから2つのことが大きく変わります。

コンポーネントとstyleのマッピングが無くなる

styled-componentsで定義するスタイル(= styled-component)はReactのコンポーネントそのものです。この仕組みにより、スクレイピング的なスタイリングではなく、レゴブロックを組み立てるようにUIを構築していくことができます。

// styled-components以前
<Button className='button'></Button>
<Button className='button button--primary'></Button>

// styled-components
<Button><Button/>
<Button primary></Button>

styled-componntsではよりシンプルに、セマンティックにすることができます。

ローカルスコープができる

CSS in JS全般に言えることですが、スコープができることにより、長い命名に悩まされる必要がなくなります。

具体的には、ファイル単位、コンポーネント単位のスコープとなるので、下記のようなBookコンポーネントとBookListコンポーネントがあった時に、どちらのコンポーネントでもWrapperという名前のコンポーネントを定義して使うことができます。

// Book.js
export default ({ book }) => (
  <Wrapper>
    <Title>{book.title}</Title>
    <Author>{book.author}</Author>
  </Wrapper>
)

const Wrapper = styled.div`
  /* Book固有のWrapperスタイル */
`
const Title = styled.h2``
const Author = styled.p``
// BookList.js
import Book from './Book'

export default ({bookList}) => (
  <Wrapper>
    {bookList.map(book => <Book book={book}/>)}
  </Wrapper>
)

const Wrapper = styled.div`
    /* BookList固有のWrapperスタイル */
`

styled-componentsで変わらないこと

これまでと変わらないこともあります。枚挙にいとまがないので、簡単に列挙します。

  • cssそのもの記述
    • タグ付きテンプレートリテラルの中に書くcssは従来のcssそのものです。
  • 定数管理、mixinなどの設計
    • sass等で実践されていた、スタイルに関する定数やよく使うスタイルをmixinとして切り出すといった考え方は依然として必要になります。
  • 見た目とレイアウトを分ける、などの基本的な設計
    • 言語がcssからjsに移るものの、最終的にはピュアなcssとして吐き出されるので、css的に必要な設計は変わりません。

styled-componentsで最低限考えるべき設計

それでは、上記の変わること/変わらないことを踏まえた上で、どのような設計が必要になるでしょうか。ここでは最低限押さえておきたいポイントを紹介します。

フォルダ構成

どうコンポーネントを組み立てるかという話です。
Styled-componentsの作者の提案する構成は、ページやコンポーネント固有のものと複数のコンポーネントから使用される共通コンポーネントに大きくディレクトリを分けるアプローチです。

src
├── App
│   ├── Header
│   │   ├── Logo.js    
│   │   ├── Title.js   
│   │   ├── Subtitle.js
│   │   └── index.js
│   └── Footer
│       ├── List.js
│       ├── ListItem.js
│       ├── Wrapper.js
│       └── index.js
├── shared
│   ├── Button.js
│   ├── Card.js
│   ├── InfiniteList.js
│   ├── EmojiPicker
│   └── Icons
└── index.js

css - Styled-components organization - Stack Overflow
このアプローチはシンプルさが強みで、自分も小規模〜中規模なアプリケーションではこの構造に習っています。

一方で、アプリケーションの規模が大きくなるともう少し構造化したくなります。その場合、下記のようにAtomicデザイン的な構成を取るのがおすすめです。(コンポーネント名等は適当に書きました)

src
├── atoms
│   ├── button
│   │   ├── button.js
│   │   ├── index.js
│   │   └── extended-button.js
│   ├── heading
│   │   ├── h1.js
│   │   ├── h2.js
│   │   ├── h3.js
│   │   ├── index.js
│   ├── icons
│   │   ├── icons.js
│   │   ├── index.js
│   ├── input
│   └── ├── checkbox.js
│       ├── index.js
│       ├── text-input.js
│       └── textarea.js
├── molecules
│   ├── calendar
│   │   ├── calendar.js
│   │   ├── index.js
│   │   └── week.js
│   ├── modal
│   └── ├── index.js
│       └── modal.js
├── organisms
│   ├── footer
│   └── ├── footer.js
│       └── index.js
├── pages
│   ├── users
│   │   ├── users.js
│   │   ├── friend.js
│   │   └── index.js
│   ├── books
│   │   ├── books.js
│   │   ├── index.js
│   │   └── book.js
│   └── login
│       ├── login.js
│       └── index.js
└── templates
    ├── default-template
    │   └── index.js
    └── specific-template
        └── index.js

styled-componentsは小さなUIからレゴブロックの用に構築していくという設計哲学上、atomicデザインと親和性が高いです。また、自分は各コンポーネントのディレクトリにindex.storie.jsを置き、storybookでスタイルガイドが見れるようにすることが多いです。

ファイル構成

何をどこに書くのかという話です。
Jsxとstyled-componentsの記述を下記のように分けて書くスタイルもよく見けます。Css modulesを使ったことのある人はこちらに慣れているのではと思います。

Card
├── index.js
└── style.js

個人的には、これらは分けずに同一ファイルに書く方がstyled-componentsの思想とマッチして好みです。ただし、ファイルを見た時にjsxの構造がわかるよう、jsx -> styled-componentsという順番で書いています。

// Componentが先
const Card = props => (
    <Wrapper {...props}>
        <Something>{props.children}</Something>
    </Wrapper>
)

// styled-compoenentsは後
const Wrapper = styled.div``
const Something = styled.span``

命名ルール

ファイル単位、コンポーネント単位のスコープができ、冗長な命名は不要になりましたが、各コンポーネント内の要素の命名はリーダビリティの観点から一貫性のあるものにすべきです。自由な命名ができるからこそ、セマンティックな命名を心がけます。

  • コンポーネントのラッパー:Wrapper, Box, Containerなど
  • 個-集合の関係にあるコンポーネント:Item-ItemList、Item-Items、Item-ItemGroupなど
  • レイアウト用途のもの:Flex, Fixed等

また、コンポーネントに渡すpropsの命名にも気をつけたいです。

  • 型が推測し易い命名にする:isActive, hasHeaderなど(booleanを渡すと推測できる)
  • htmlのアトリビュートとしてあるものはそのまま使う:disabled, checkedなど

定数を管理する

色やサイズは定数で管理しましょう。styled-componentsはjsなので、React側と定数を共通で管理できるのが良いところです。
オブジェクトとして定義し、タグ付きテンプレートの中で${}で囲んで使うようにします。

色であれば、下記のようになります。

// const/Color.js
export default {
  PRIMARY: '#XXXXXX',
    SECONDARY: '#XXXXXX',
    DANGER: '#XXXXXX'
}

// styled-components
const ErrorMessage = styled`
    color: ${Color.DANGER};
`

サイズであれば、計算等し易いようにpxを除いて数値で管理します。

// const/Size.js
export default {
    FONT: { SMALL: 10, BASE: 12, LARGE: 16 },
    HEADER_HEIGHT: 40
}

// styld-components
const H2 = styled.h2`
    font-size: ${Size.FONT.LARGE}px;
`

拡張性を高める

Styled-componentsでのコンポーネントの再利用は幾つかの方法がありますが、次のような優先順位で考えると良いでしょう。
1. propsでstyleを出し分ける
2. styled()またはextendで拡張する
3. 別のコンポーネントにする

例えば、ボタンであればpropsを使って<Button primary /> <Button secondary /> <Button danger /> のように使い分けることができます。このような場合に、<ButtonPrimary /> <ButtonSecondary /> <ButtonDanger /> のように分けることはあまりすべきではありません。

また、やや話は逸れますがstyled-componentsで受け取るpropsはインターフェースとしてわかりやすくすべきです。例えば、大きさの違うモーダルがある時、下記のようにstringを渡す受け口を作るのもflowやproptypesを使う場合に良い方法です。

// より明示的。flowやproptypesの恩恵を受けられる
<Modal appearance='large' />
<Modal appearance='medium' />

// flow
type Props = {
    appearance: 'medium' | 'large'
}

// 自由すぎる、他のpropsとの競合もありえる
<Modal large />
<Modal medium />

Propsで分けるには見た目に違いが大きい場合や、用途が例外的な場合は拡張をしましょう。

// 通常のButton
const Button = styled.button`
  padding: 12px;
  border-radius: 4px;
`

// コンバージョンだけに使うButton。
// extendはv4.0で非推奨になるので使わないようにしましょう
const ConversionButton = styled(Button)`
  padding: 24px;
  font-weight: 600;
`

拡張をするメリットは、primitiveである元のコンポーネントのコードを汚さないことです。ただし、拡張する側は元のコンポーネントに影響されるので、元のコンポーネントに変更を加える場合は注意が必要です。あまりに見た目や用途が異なる場合は別のコンポーネントとして作ることも考えましょう。

スタイルとレイアウトを分けて考える(続編)

最も重要でstyled-componentsのコミュニティ内でも議論が多いポイントなのですが、これだけで1記事書けてしまうので別の機会に書きたいと思います。

まとめ

タイトルをcss設計としておきながら、実際にはコンポーネントそのものの設計に近い話になりました。実際、CSS in JSに対する否定的な意見として、「デザイナーが書けない」というものがあるのですが、Reactの世界においてスタイルとコンポーネントの設計はもはや分けられるものではありません。

コンポーネント時代のstylingを変えたstyled-components,おすすめです。

更新履歴

20180604: コンポーネントのstyle拡張のために使えるextend``APIはv4で非推奨になる予定のため、例から外しstyled()での拡張に置き換えました。
see https://github.com/styled-components/styled-components/issues/1546