Edited at

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


余計な命名に悩まないようにする

最近はGrid LayoutやFlexboxで無駄なdivを作らずに済むことも多いですが、実際にはFooWrapperやBarWrapperのようなレイアウトのためだけのコンポーネントがどうしても必要な場合があります。「marginを少しつけたい」くらいなのにわざわざコンポーネント名を考えたくはないですよね。

styled-systemのようなライブラリを使うのも一つですが、styled-components


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

最も重要で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