Storybook で考えるコンポーネントについて (React/Redux)

年の終わりと始まりということもあって、フロントエンドっぽい記事を一つ。
私は ユニバ という会社でフロントエンド業を主にやっています。ユニバでは今年のアプリケーション開発では React/Redux を使ってやってきました。
そこで開発していく上で必須となったものが Storybook でした。

今回は https://github.com/monpy/react-storybook-try にサンプルを用意しました。 Storybook も ver4 になってから色々変更があったのでいい勉強になりました。

なぜ Storybook を使うのか

こういう話は他でも色々とありますが、、、
私の経験の中で使った方がいいシチュエーションがあります。

ある程度大きなアプリケーションを開発し、運用していくとき
そもそも React を使っている時点で大きなアプリケーションかとは思いますが、単純にコンポーネントを開発する人数が複数人なら、移植前のチェックに便利です。また、デザイナーと仕様について、この場合はレスポンシブなコンポーネントのチェックやカラーリングなど、を運用含めて折り合いをつけたいときには多少面倒でも利用することをお勧めします。

制作の進行上、コンテナなどの開発とコンポーネントの開発を同時進行で進めたいとき
React/Redux を使っているということは、Store からデータを流し込む必要などが出てきます。ここでいうコンテナというのは 主に Connect する部分を指ます。このコンテナの実装を誰がやるか?などの課題はそれぞれだと思いますが、基本的にはコンテナの実装が固まるまでコンポーネント作りが多少面倒になるケースが多いです。その点 Storybook 上ではそういったしがらみからは別に開発を進行することができます。これはフロントエンドとバックエンドの進行にとってはとても喜ばしいことですし、Storybook はフロントの仕様書になる側面もあるのでより円滑に開発を進めることができるでしょう。

上記2つ以外のケース。それほど大きいプロジェクトでもなければ面倒を見る必要がない。React パートは俺が全部書くんだ。という方は Storybook を使わずにゴリゴリそのまま効率が良いと思います。Storybook を作り上げるというのも凄いコストですからね。個人的には Storybook でコンポーネントをテストしながら移植して開発するというサイクルは素晴らしいと思っているので余裕があればやってみることをお勧めします。

Storybook で出てくる3つのコンポーネント

ここからは Storybook を使うと決めた後の話を書いていきます。
Storybook は主にコンポーネントの開発をしていきます。(おまけでグリッドシステムとかカラーマネージング・タイポグラフィの見本表とか作ります。)
作るべきコンポーネントは主に3つです。

- 単純コンポーネント
- グルメコンポーネント
- 偏食コンポーネント

だと私は考えてます。
食事に例えたのはコンポーネント は props という餌を与えられて動くものだと思っているからです。本当は小鳥さんかなと思っているんですが(実装者は親鳥)、いらすとやが便利だからね。仕方ないね。

今回は アプリケーションにありがちなユーザ情報を反映させた Avatar コンポーネントの開発を例にあげます。
 ( https://github.com/monpy/react-storybook-try ここにサンプル置いてますのでよろしかったら実際動かしてみてください。大したものは何もないですが。。。)

単純コンポーネント

これが一番作りやすく、単純で、ここから始まるといってもいいコンポーネントです。何が単純なのかといえば、送られてくる値が ほとんどプリミティブな値しかやってこない。そしてそれを写すだけ。というものです。

import React from 'react';

export default ({ name, role }) => {
  return (
    <p>
      {name} / {role || 'guest'}
    </p>
  );
};

上の例は非常に単純で user の name と role をもらってそれをそのまま情報として出力する。というコンポーネントです。

単純コンポーネントはしばしば デザイン構造上の末端によく使われます。アトミックデザインの言葉を使えば原子のレベルで取り扱われる、ラベルだったりボタンだったりインプットだったり。そういうものが多いです。

このコンポーネントの一番のメリットはその単純明快さにあります。作ったものが仕様となりコンポーネントに値を渡す部分でのみで問題を解決すればいいからです。起こる問題としては、欲しい値が来ていない。それだけで済むのが魅力です。

単純コンポーネントでアプリケーションの全てのパーツを作ることも可能です。しかしオススメはしません。
React 経験者あるあるである props のバケツリレー問題にすぐに陥ります。もしこのコンポーネントの表示に必要な情報が増えたら、このコンポーネントに情報を渡す親コンポーネントの回収が必要になります。そうしてどんどん修正箇所が増えてっては生き地獄になる。

この問題は次に説明するグルメコンポーネントを設計することで解決されます。

グルメコンポーネント

上の単純コンポーネントの例を進めて少しグルメにします。

import React, { Component, SFC } from 'react';
import { User } from '../../domain/user';

type Props = {
  user?: User;
};

export const Avatar: SFC<Props> = props => {
  const { user } = props;
  if (!user) return <p>no user object</p>;
  const { role, name } = user;
  return (
    <p>
      {name} / {role || 'guest'}
    </p>
  );
};

グルメというのは、もらう値が オブジェクトになり、さらに踏み込めば型になるということです。

Avatar がもらう 値を User 情報ということを前提に開発し、その中には上で使われた値が入っている。という仕様にします。

こうすることで、User 型に必要な情報が入っている限り、コンポーネントの中で問題の解決が行えることができます。この理由から多少の仕様の変更に強いコンポーネントが作ることができます。

非常に魅力的なグルメコンポーネントですが、作るまでの労力は大変です。

- 正しい型を作らないといけない
- それを正しく利用できるフレームワークを導入しなければいけない

というのが主な困難です。言ってしまえば TypeScript など型を設定できる中間言語の利用です。
もちろんピュアな javascript だけでも可能な部分はありますが、あくまでそれっぽく書くだけで実際動くまで制約が守られているかの保証がされないという問題と睨めっこしなければいけません。
その点 TypeScript を使えばエディタの補完機能やコンパイル時の規約チェックなど、受けられる恩恵がとても大きいです。

こうしてできたグルメコンポーネントはアプリケーションのなかで優れた活躍を見せるはずです。

そして最後に紹介するのは、Redux の環境の中で出てくる偏食コンポーネントです。

偏食コンポーネント

Storybook で重要なのは、コンポーネントに正しく値を渡せば、アプリケーション側でも Storybook 側でも正しく動作することです。
この時重要になるのが、Redux アプリ側で値を注入されたコンポーネントを内包するコンポーネントです。

具体的にどういうことか?
今まで作ってきた Avatar コンポーネントはアプリ側ではログイン後に Store 側から常にユーザ情報が直接渡ってくるものとして考えます。

import { Avatar } from '../avatar';
import { connect } from 'react-redux';

function mapStateToProps(state, props) {
  return {
    user: state.auth
  };
}

export default connect(mapStateToProps)(Avatar);

こう言った状況はしばしばアプリケーションではあります。いちいち全てのビューで親から渡すくらいなら個別で connect する方が効率いいです。
ですが、これによってこのコンポーネントは Redux というシステムにフィットしたコンポーネントになってしまいました。

これを満足に Storybook 上で動かすには、Storybook 上でRedux のシステムを利用しながら開発できるようにしなければいけません。

const stories = storiesOf('Header', module);

let user: User = {
  id: 1,
  role: 'role-example',
  name: 'name-example'
};

const store = redux(stories, { auth: user });

かなり割愛しますが、具体的には redux の initialState を利用して createStore の時に値を注入することで解決をします。

ここまで至るには、バックエンドの人などとの協力が必須になりますし、Storybook で重要なアドオンでもある knobs による値のダイナミックなテストが難しくなってしまいます。
上記の理由から基本的には偏食コンポーネントではレイアウトのテストが主になるかと思います。
レイアウトのテストもしつつ、効率の良い、アプリと同じ構造を維持したコンポーネントのテストはかなり重要です。
浅いコンポーネントだけだとフロントとしてのテストの意味が十分でなくなってしまう。
かと言って単純コンポーネントばかりやっていると移植の際にかなり苦労します。
ここの折り合いをチームの中で検討するというのがフロントエンドとしての腕の見せ所だと思います。

最後に

以上が私の中で Storybook で開発を行う時に登場するコンポーネント3種類でした。
私はこういうスタイルで開発してるよ〜みたいな人がいたら意見聞いてみたいですね。
フロントの仕事はアプリケーションの仕様に合わせて柔軟に対応するの大事なのですが、それを実現するのが Storybook だなと私は考えています。

いくつか Storybook はいいよという記事はあったのですが、私はこう使っているという例がもっと増えると幸せですね。
勉強会開きて〜


この記事が気に入ったらサポートをしてみませんか?