Edited at

これからのReactコンポーネントのスタイリングはstyled-componentsが良さそう

More than 1 year has passed since last update.

前々回のprettier前回のflowに引き続きReact Confで紹介されてたものを使うシリーズ第3弾です。

styled-componentsはSassやRadium、CSS Moduleといったスタイル系ライブラリの問題を解決するために生まれた新しいライブラリで、今Reactコンポーネントのスタイリングをするならベストの選択肢なんじゃないかと思います。


追記


  • 2017/4/2 - jestでのテストについて追記しました。

  • 2017/5/26 - v2について簡単に記載しました。


styled-componentsとは


  • styled-components

  • 所謂CSS in JSの一種でjsx内に普通にcssが書ける。


    • キャメルケースとかせずにcssが書けるので、(複雑なことをしない限りは)移行が楽。



  • props渡したりテーマ作ったり、extendできたり機能豊富。

  • 擬似要素やmedia queryといったCSSでできることはほぼ(全部?)サポートされている。

  • ベンダープレフィックスも自動で付く。(が、いらない場合に消したいときもある...)

  • SSRやReact Nativeにも対応。

  • 少し前に流行ったCSS Moduleとさらにその前に流行ったRadiumとスター数を比較するとこんな感じ。かなり勢いがあるのが分かる。


  • この記事がCSS -> SASS -> BEM -> CSS Module -> styled-componentsへの遷移をよく説明していると思う。

  • エディタ連携も豊富

  • flowとかlintのサポートもあるけどまだ安定してない。

  • CSSを使っていた場合はJestのsnapshotテストでクラス名しかテストできなかったが、スタイルの中身をテストできるようになる。

  • (CSS in JS系全般に言えることだけど)パフォーマンスへの影響の懸念はある。


使い方

前回の続きからeslint、prettier、flowが入ったところに追加します。

ソースはここにあります。

英語問題ない人は公式サイト見たほうがいいと思いますw


前準備

前回まではReact使わずに来たので、まずはReactを入れます。既にReact使ってるプロジェクトなら飛ばしてください。

というかReactのインストールは色んなところで紹介されてる&本題では無い&自分の構成は現在のベストプラクティスでは無いので詳細は書きません。

React入れた分のdiffはこのコミット見てもらえればと思います。


cssの代わりにスタイルしてみる

まずはstyled-componentsをインストールします。

yarn add styled-components

そして以下のようにstyled-componentを使ったcomponentを作ります。


Container.jsx

import styled from 'styled-components';

const Container = styled.article`
background: #F6F6F6;
width: 100%;
height: 100vh;
`
;

export default Container;



Header.jsx

import styled from 'styled-components';

const Header = styled.header`
font-size: 1.5rem;
padding: 2rem;
text-align: center;
color: #FFC507;
background: #353B41;
`
;

export default Header;


素のcssなのでかなり分かりやすいんじゃないでしょうか。

そしてこれらを使うコンテナを作ります。


App.jsx

import React from 'react';

import {Container, Header} from './components';

const App = () => (
<Container>
<Header>Title</Header>
</Container>
);

export default App;


するとこのような結果になります。

Screenshot from 2017-03-25 00-20-33.png

かなり直感的に使えます。


propsを渡す

こんな感じでpropsを使うこともできます。


DefaultButton.jsx

import styled from 'styled-components';

const Button = styled.button`
background:
${props => props.active ? 'gray' : 'white'};
color:
${props => props.active ? 'white' : 'black'};
margin: 1rem;
font-size: 2rem;
padding: 0.25rem 1rem;
border: 2px solid
${props => props.active ? 'transparent' : 'black'};
border-radius: 3px;
&:hover {
cursor: pointer;
}
`
;

export default Button;


ちなみに上記の通り擬似要素なども問題なく使えます。

そして使う側では普通にpropsを渡せばOKです。


Main.jsx

class Main extends React.Component {

constructor(props) {
super(props);
this.state = {
active: false,
};
}

toggleButton() {
this.setState({
active: !this.state.active,
});
}

render() {
return (
<Container>
<Buttons>
<DefaultButton
onClick={() => this.toggleButton()}
active={this.state.active}
>
Button
</DefaultButton>
</Buttons>
</Container>
);
}
}


するとこんな感じのボタンになります。

Untitled.png


Overrideする

コンポーネントのOverrideもできます。

上記で作ったDefaultButtonをOverrideして以下のような文字サイズを倍にしたボタンを作ってみます。


BigButton.jsx

import styled from 'styled-components';

import DefaultButton from './DefaultButton';

const Button = styled(DefaultButton)`
font-size: 4rem;
`
;

export default Button;


するとこんな感じのボタンを作ることができます。

フォントサイズ以外はOverrideした元のコンポーネントのスタイルが適用されていることが分かります。

Screenshot from 2017-03-25 11-53-09.png


Themeを使う

Themeを使って共通のスタイルを適用することもできます。

まずは以下のようなGreenButtonsというコンポーネントを作ります。


GreenButtons.jsx

import React from 'react';

import {ThemeProvider} from 'styled-components';
import Buttons from './Buttons';

const theme = {
main: 'mediumseagreen',
};

const GreenButtons = props => {
return (
<ThemeProvider theme={theme}>
<Buttons>
{props.children}
</Buttons>
</ThemeProvider>
);
};

GreenButtons.propTypes = {
children: React.PropTypes.element,
};

export default GreenButtons;


そしてThemeを使うButtonを作ります。


ThemedButtons.jsx

import styled from 'styled-components';

import DefaultButton from './DefaultButton';

const ThemedButton = styled(DefaultButton)`
background:
${props => props.theme.main};
`
;

ThemedButton.defaultProps = {
theme: {
main: 'palevioletred',
},
};

export default ThemedButton;


上記の通り、Themeが指定されなかった場合に備えてdefaultPropsを指定することもできます。

そして使う側で以下のようにGreenButtonsコンポーネント囲うとテーマが適用されたButtonになります。


Main.jsx

<GreenButtons>

<ThemedButton>
Green Button
</ThemedButton>
</GreenButtons>

Screenshot from 2017-03-25 12-23-01.png

Themeを使ったコンポーネントで囲わないと、defaultPropsで与えた色のButtonになります。


Main.jsx

<GreenButtons>

<ThemedButton>
Green Button
</ThemedButton>
</GreenButtons>
<Buttons>
<ThemedButton>
Default Themed Button
</ThemedButton>
</Buttons>

Screenshot from 2017-03-25 12-23-07.png


他ツールとの連携


lint

stylelintと連携するstylelint-processor-styled-componentsを使うとstyled-componentのスタイルをチェックすることができます。(まだexperimental)

まずは必要なパッケージをインストールします。

yarn add -D stylelint-processor-styled-components stylelint stylelint-config-standard

そして以下の内容でプロジェクトルートに.stylelintrcを作ります。


.stylelintrc

{

"processors": ["stylelint-processor-styled-components"],
"extends": "stylelint-config-standard",
"syntax": "scss"
}

そして以下のように実行するとエラーがある場合はエラー内容を出力してくれます。

Screenshot from 2017-03-25 14-01-17.png

stylelintはstylefmtと連携することで自動整形もしてくれますが、styled-componentのissueでprettierによる整形が議論されてるので、今回は見送りました。あとで方針が固まったら追記します。


flow

このページにある通りflowとの連携もできます。

が、現状はその通りに設定してもこのissueで触れられている問題とこのissueで触れられている問題に当たってしまい有効にできなかったため一旦見送りました。こちらも解決したあとにまた追記したいと思います。


vim

vim-styled-componentsを使うとシンタックスハイライトが使えます。

deinとかでインストールして有効にするだけでシンタックスハイライトが効きます。


deinlazy.toml

[[plugins]]

repo = 'fleischie/vim-styled-components'
on_ft = ['javascript', 'javascript.jsx']


jest

jestのsnapshotテストはstyled-componentと相性抜群です。外部css(sass)に書くとクラス名しかテストできずあまり意味のないテストになっているのが課題でした。

しかしstyled-componentsを使うとスタイルがすべてテストできるようになります。

まずは必要なライブラリを追加します。

yarn add -D jest react-test-renderer jest-styled-components

.eslintrc.jsを編集してjest関連に対応します。


.eslintrc.js

  env: {

browser: true,
jest: true,
},

package.jsonにjestのセクションを作り、以下のように指定します。


package.json

  "jest": {

"bail": true,
"testEnvironment": "node",
"rootDir": "src"
},

そしてDefaultButton.jsxと同じディレクトリに以下のようなファイルを作成します。


DefaultButton.test.jsx

import React from 'react';

import {matcher, serializer} from 'jest-styled-components';
import renderer from 'react-test-renderer';
import DefaultButton from './DefaultButton';

expect.extend(matcher);
expect.addSnapshotSerializer(serializer);

describe('DefaultButton Test', () => {
it('should be rendered correctly on active', () => {
const renderedElement = renderer.create(<DefaultButton active={true} />);
expect(renderedElement).toMatchStyledComponentsSnapshot();
});
it('should be rendered correctly on disabled', () => {
const renderedElement = renderer.create(<DefaultButton active={false} />);
expect(renderedElement).toMatchStyledComponentsSnapshot();
});
});


そしてプロジェクトのトップでjestコマンドを実行すると以下のようなsnapshotファイルが作成されます。


DefaultButton.test.jsx.snap


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DefaultButton Test should be rendered correctly on active 1`] = `
.jXtbih {
background: gray;
color: white;
margin: 1rem;
font-size: 2rem;
padding: 0.25rem 1rem;
border: 2px solid transparent;
border-radius: 3px;
}

.jXtbih:hover {
cursor: pointer;
}

<button
className="jXtbih"
/>
`;
(略)


スタイル自体がテストできることが分かります。かなり便利ですね!

ただし、欠点があって現状はDOM操作ができないので、挙動のテストができません。

なのでスタイルも挙動もテストするとなると別々のテストファイルとpackage.json作ってテストするとかめちゃくちゃな対応が必要になります...中々でかい欠点ですね...

ただissueが上がってるのでその内解決するんじゃないかと思います。

解消したらまた追記したいと思います。


v2

2017/5/25にv2が発表されたのでv2の新機能を簡単に紹介します。


extend

extendを使うと既存のコンポーネントを拡張できます。

もともと上記のOverrideするで書いた通りstyled(AnotherComponent)みたいな書き方でできましたが、その場合は二つのクラス名が追加されてしまう、両方のコンポーネントがReactのrender tree以下にできてしまうといった問題があるため、extendが推奨らしいです。

上記の続きの例としてExtendedButtonを作ると以下のような感じで作ることができます。


ExtendedButton.jsx

import DefaultButton from './DefaultButton';

const ExtendedButon = DefaultButton.extend`
background: midnightblue;
font-size: 4rem;
color: white;
&:hover { text-shadow: 0 0 4px white; }
`
;

export default ExtendedButon;


Screenshot from 2017-05-26 15-04-36.png


attrs

attrsで属性を渡すことが可能になりました。

例えばクラス名を与えればglobalに定義されたCSSのクラスを当てることができます。

const Header = styled.header.attrs({

className: 'p2 bold white bg-blue'
})`
...
`

attrよってReact RouterのLinkコンポーネントのactiveClassNameに値を渡したりできるようになります。


withComponent

withComponentは別のタグに同じスタイルを使いたいみたいな状況で役立ちます。

const Heading = styled.h1`

/* ... */
`

export const H1 = Heading
export const H2 = Heading.withComponent('h2')


SSRがサポートされた

自分はSSRはなるべくしたくない派なので使い方は公式を見てくださいということで...

とりあえず必要最低限のCSSしか送らない、一度injectされたCSSは再renderしないみたいな作りになってるらしいです。

ということで、簡単にv2の紹介でした。詳しくは最初に記載したブログポスト新しくなった公式サイトを見てもらえればと思います。


最後に

以上、参考になれば幸いです。

また必要に応じて追記・更新したいと思います。