react-storybookを用いたReactコンポーネント開発

こんにちは!Webアプリケーションエンジニアの id:amagitakayosi です。
今日はReactコンポーネントを手軽に開発するためのツールを紹介します。

前回のあらすじ

developer.hatenastaff.com

前回の記事では、Reactコンポーネントをnpmパッケージとして開発する方法を紹介しました。
対象としたのはこちらの無限スクロール用のReactコンポーネント。

http://www.npmjs.org/package/@fand/react-infinite-scroll-container

このパッケージでは、 example/ に確認用プロジェクトを作成して動作確認を行っていました。
npm link を利用することで、パッケージを npm publish する前に実際の動作を確認することができます。
また、パッケージ本体の package.json が複雑になるのを防げるというメリットも存在します。

一方、この手法では、プロジェクト間の依存管理や npm link の挙動など、意識すべきことが多くなってしまうという問題がありました。
題材となったReactコンポーネントは非常に単純なものでしたが、プロジェクト構成を理解するにははnpmの複雑な挙動を知る必要があります。
その上、動作確認のコードや設定ファイルのため、開発者は本質的でない部分に時間を取られてしまいます。

今回は react-storybook というツールを使い、開発者が余計なことを考えず、Reactコンポーネントの開発に集中できる環境を作っていきます。

react-storybookとは

github.com

先日、react-storybookというReactコンポーネント開発用ツールが公開されました。
開発元はkadira。Meteorアプリのパフォーマンス監視ツール等を提供している会社です。

ツールの公開まもなく、使い方を紹介するブログ記事がMediumに投稿され、話題になりました。
Hacker Newsにも登場したので、すでにご存知の方もいらっしゃるかもしれません。

Introducing React Storybook — KADIRA VOICE

react-storybookは、汎用的で独立したReactコンポーネントを開発するためのツールです。
後述するHot Module Reloading (HMR)の仕組みにより、コーディングと動作確認がスムーズに行えます。
また、コンポーネントの使いかたを記述した story という単位で開発を進めることで、コンポーネントの挙動や役割がより明確になります。

導入してみる

この章では、前回紹介したプロジェクトにreact-storybookを導入していきます。
前記事の時点でのコードはこちら。 https://github.com/fand/react-infinite-scroll-container/tree/abf60f25a8c29c2f3948b79de0a3eeb17fd12afb

example/ は不要なので削除します。 以下の package.json のハックも不要になるので消しておきます。

- "pretest": "npm ls react || npm i react",
- "prebuild": "npm ls react && npm rm react",
初期設定

react-storybook をインストールし、設定ファイルを作成します。

npm i -D react-storybook

設定ファイルは .storybook/config.js に置きます。
コンポーネントが複数あるときは、 loadStories 内を書き換えて下さい。

// .storybook/config.js
import { configure } from '@kadira/storybook';

function loadStories() {
  require('../stories/InfiniteScrollContainer');
}

configure(loadStories, module);
storyを作成する

次にstoryファイルを作成します。
storyファイルには、実際に想定されるコンポーネントの利用例を記述します。
今回は、スクロールイベントが発生する場合、及び発生しない場合の2通りのstoryを用意しました。
stories/InfiniteScrollContainer.js を以下の内容で作成します。

import React from 'react';
import { storiesOf, action } from '@kadira/storybook';
import { range } from 'lodash';

import InfiniteScrollContainer from '../src/InfiniteScrollContainer';

const STYLE = {
  wrapper : { position: 'relative', width: 300, height: 500 },
  item    : { height: 98, border: '1px solid red' },  
};

storiesOf('InfiniteScroll', module)
  .add('No scroll', () => (
    <div style={STYLE.wrapper}>
      <InfiniteScrollContainer onScroll={action('scrolled')}>
        {range(3).map(i => <div style={STYLE.item} key={i}>{i}</div>)}
      </InfiniteScrollContainer>
    </div>
  ))
  .add('scroll', () => (
    <div style={STYLE.wrapper}>    
      <InfiniteScrollContainer onScroll={action('scrolled')}>
        {range(10).map(i => <div style={STYLE.item} key={i}>{i}</div>)}
      </InfiniteScrollContainer>
    </div>
  ));
起動

それでは、storybookを起動してみましょう。

$(npm bin)/start-storybook -p 9001

f:id:amagitakayosi:20160413184959g:plain

無事に起動しました!
左側のペインでstoryの切り替えができます。

続いて、storybookのウリの一つであるHot Module Reloadingを試して見ましょう。
storybookが起動した状態で、storyファイルを編集してみます。

f:id:amagitakayosi:20160413185104g:plain

ファイルの更新が反映されていますね!

イベントを監視する

react-storybookには、簡単なロガーが組み込まれています。
開発者は、story内のログを取りたい場所で action を実行します。
この時 action に渡した引数が、画面下の ACTION LOGGER に表示されるという仕組みです。

今回のstoryファイルでは、 onScroll={action('scrolled')} とすることで、コンポーネントのスクロール時にactionが発火するようにしています。 実際のログの表示を確かめてみましょう。

f:id:amagitakayosi:20160413185156g:plain

おや……上にスクロールしたときもactionが発火している……
前回実装したInfiniteScrollContainerは、スクロールの方向を考慮するのを忘れていたようです!

スクロール位置を記録し、下方向のスクロール時のみイベントが発火するように修正しました
ちゃんと直っているか確認します。

f:id:amagitakayosi:20160413185317g:plain

上にスクロールしてもactionが呼ばれていません。大丈夫ですね。
コンポーネントがコールバックを受け取ったりイベントを発行したりする場合、なるべくactionで記録するようにしておくと、細かいバグの発見に役立つでしょう。

Hot Module Reloadingの仕組み

react-storybookは、Hot Module Reloadingをどのように実現しているのでしょうか?
実は、Hot Module Reloadingの仕組み自体は、Webpackのものをそのまま利用しています。

Hot Module ReloadingはWebpackの代表的な機能の一つです。
この機能を利用するには、クライアント側でランタイムをロードするだけでなく、専用のサーバーを立てるか、express用のミドルウェアを利用する必要があります。
ランタイムはモジュールの依存グラフを記憶しており、一部のモジュールを入れ替えてアプリケーション全体を再実行することができます。
そのため、ページ全体をリロードすることなく、モジュール単位で更新できるのです。 (参考: https://webpack.github.io/docs/hot-module-replacement-with-webpack.html)

実際にサーバーとランタイムの通信内容を見てみましょう。

こちらはreact-storybook起動直後の通信内容です。
react-storybookは、それ自体がexpress + Reactで作られたWebアプリになっています。
プレビュー領域はiframeになっているため、親フレームと子フレームそれぞれHTMLとJSを読み込んでいることがわかります。

ページ表示直後の通信内容

リスト内には __webpack_hmr という項目も存在します。
これはファイルの更新通知に用いられるServer Sent Eventsのストリームです。
更新のない時は、接続維持のため定期的にハートビートが送られています💓

ハートビート通信の様子

ファイルが更新されると、サーバーは差分のモジュールをビルドし、クライアントに更新通知を送信します。
__webpack_hmr を見ると、更新通知が送られている事がわかります。

更新時の通信の様子

通知には、ビルド時間やビルド結果のハッシュ、モジュールのリストなどが含まれています。

{
    "action": "built",
    "time": 540,
    "hash": "a24aa28afd2c731361bd",
    "warnings": [],
    "errors": [],
    "modules": {
        "0": "multi admin",
        "1": "./~/process/browser.js",
        "2": "./~/fbjs/lib/invariant.js",

        // 中略

        "302": "./.storybook/config.js",
        "303": "./src/InfiniteScrollContainer.js",
        "304": "./stories/InfiniteScrollContainer.js",

        // 中略

        "319": "(webpack)-hot-middleware/process-update.js"
    }
}

その後、ランタイムは新たにmanifest, 及び差分のJSファイルをロードします。

更新の様子

manifestの内容は以下のとおり。
h はビルド結果のハッシュです。通知のハッシュと一致していますね。
c は更新されたモジュール数のようです(ソース)。

{"h":"a24aa28afd2c731361bd","c":[1]}

JSファイルには、更新されたモジュールの番号、および更新の内容が含まれています。
今回更新したファイルは stories/InfiniteScrollContainer.js でした。
この番号を更新通知内のモジュールリストと照らし合わせると、ファイルの更新が正しく反映されていることがわかりますね。

gist.github.com

WebpackのHMRでは、モジュール更新時の差し替え処理を開発者が設定する必要があります。
ただし、react-storybookでは、この処理はreact-storybook/src/client/config_api.jsであらかじめ設定されています。
Reactコンポーネントやstoryファイルの依存関係はこのファイルにまとめられるため、これらのファイルの更新時は同じ差し替え処理を使いまわせます。
結果、開発者はHMRの詳細を考えることなく、Reactコンポーネントの開発にだけ専念できるというわけです。

HMRについてより詳しく知りたい方は、以下のリンクをご覧ください。

メリットとデメリット

react-storybookを利用した開発のメリット・デメリットを考えてみます。

リアルタイム確認できる

HMRのおかげで、コーディングと動作確認が同時に行えます。
コンポーネントやstoryの修正がリアルタイムに反映されるので、karmaでテストファイルをautoWatchしている感覚に近いです。

導入が簡単

GitHubをあさると、HMRに対応したReactコンポーネントのボイラープレートは山程見つかります。
しかし、作者の好みが強すぎたり、ボイラープレート自体が古びてしまうことも多いです。
react-storybookはあくまでパッケージとしてまとまっているため、既存のプロジェクトへの導入もたやすく、必要になればいつでも捨てられるというメリットがあります。

ストーリー駆動開発できる

react-storybookでは、事前にstoryを書き、それからReactコンポーネントを実装する、という手順で開発を進めることができます。
テスト駆動開発やREADME駆動開発など、コード例を先に書いてから開発している方も多いと思いますが、react-storybookではそうしたコード例を実際に動かして確認できるというメリットがあります。

デメリット: Webpackの機能で動いてる

react-storybookはWebpackの機能を駆使したプロジェクトです。
基本的な使い方を利用する限りでは、Webpackの設定ファイルを書いたりする必要はありません。
しかし、複雑なコンポーネントを作っていると、もしかしたらreact-storybookやWebpackのバグに遭遇するかもしれません。
その時は、react-storybookだけではなく、Webpack HMRの挙動を理解し、コードを読む必要があるでしょう。

まとめ

いかがでしたか?
react-storybookはまだまだ改善の余地がありますが、これまでのボイラープレートと比べると頭一つ抜けた感じがします。
これを機にReactコンポーネントの開発環境がもっと整備されていくと嬉しいですね。

それにしてもWebpackはすごいですね。
個人的にビルドツールはシンプルさ重視でbrowserify派なんですが、今回HMRの仕組みを調べてみると楽しくてついつい時間を忘れてのめり込んでしまいました。

HMRには、browserify上で動くものや、Babelのプラグイン上で動くものなども存在するので、興味のある方は調べてみると楽しいと思います。

それでは!


株式会社はてなでは、便利な開発ツールを作ったり、中身を調べたりするのが好きなエンジニアを募集しています!

hatenacorp.jp