React Storybook で変わるUI開発フロー (Redux Flavor)
SPAを考える会 (D3勉強会 2016.10.06)
by @kitaly (twitter: @kita_ly)
自己紹介 @kitaly
Twitter: @kita_ly
-
ソフトウェアエンジニア
- REST API開発 (Scala/Play)
- SPA開発 (TypeScript, Angular.js, React.js)
-
ビズリーチ HRMOS プロダクト開発部
- 採用管理 (2016年6月リリース)
- 勤怠管理, 評価管理, その他HR系サービス (Coming Soon..)
-
過去の発表資料
はじめに
React / Redux / Webpack 前提の話ですが
他のコンポーネント志向FWなどでも、ユースケースやワークフローは応用可能だと思っています
新しいツールも多く、実践"未"投入な構想段階のアイディアも多いです
### 今日紹介するコード例は大体ここにあります: ### https://github.com/k-italy/react-storybook-demo-plus
目次
- [導入] 動的UIの開発フローと課題
- [導入] 提案する解決策の概要
- [導入] Component as API
- [本題] Component開発環境としての Storybook
- [本題] Componentテスト環境としての Storybook
- [本題] Componentドキュメントツールとしての Storybook
- [Redux Flavour] Redux と Peudo-Local ライブラリ
- まとめ
[導入] 動的UIの開発フローと課題
動的UI開発の登場人物とスキルセットの違い
-
ES君
- 強み 「元々サーバーサイドで大規模アプリケーション組んでたし、EcmaScriptの最新仕様もバッチリ」
- 弱み 「HTMLはなんとか書けるけど、CSSはちょっと無理ぽ…(´・ω・`)」
-
W3C君
- 強み 「HTML・CSS・JQueryなら爆速開発まかせろ」
- 弱み 「複雑なアプリケーションは経験無いな。Immutable/Reactive/MVCとかよくわからん」
-
REST君
- (サーバーサイドの Rest API 開発してる人。今日はあんまり登場しない)
[導入] 動的UIの開発フローと課題
HTML納品スタイル
- マークアップエンジニアがHTML納品
- アプリケーションエンジニアがテンプレートに書き直しアプリに組み込む流れ
- HTMLがデータ構造とか条件分岐を考慮しきれてなかったりする
- HTML納品し直してもらうキャッチボール
- アプリケーションエンジニアが限られたHTML/CSSの知識で修正
[導入] 動的UIの開発フローと課題
ViewModel/ViewHelper納品スタイル
- アプリケーションエンジニアがViewModel/ViewHelperを納品
- マークアップエンジニアがHTMLテンプレートを書く
- 画面に必要なデータやHelperが足りないことが途中で判明したりしがち
- ViewModel/ViewHelper の実装し直し
- マークアップエンジニアがテンプレートのシンタックスを覚える必要(なおかつ汎用的な知識じゃなかったり)
[導入] 動的UIの開発フローと課題
Angular とか React 使うとどうよ?
→ 何も考えなかったら同じようなことになる
[導入] 解決の道筋
解決策: 「お前ら黙って勉強しろ」
→ というのは乱暴な正論というやつ
[導入] 解決の道筋
UI開発とアプリケーション開発の境界線付けをする
制御系の実装者とAPI提供者の区別をつける
- REST君: Rest API を提供する
- W3C君: Component API を提供する
- ES君: 上記を材料としてWebアプリケーションを組み立てる
[導入] 解決の道筋
開発フロー
- 開発者間でI/Fを握る
- ES君とREST君が Rest API の仕様で合意
- ES君とW3C君が Component API (後述)の仕様で合意
- API提供者が並行で開発
- REST君が Rest API の実装
- W3C君が Component API の実装
- ES君が制御系を実装
- Webアプリケーションが完成
[導入] 解決策の概要
React/Redux アーキテクチャ上だと
※ なお、私のプロジェクトでは Middleware部分 は Redux-Saga を使ってますRedux-Middlewareに何を使うか、Flux系FWは何を使うか、等は差し替え可能かも
※ 今日は UI Component 側の話がメインなので詳しく話さないです
View部分も、React以外のコンポーネント志向FWに差し替え可能かも
[導入] Component as API
Reactとその周辺ツールで可能になったことをおさらい
[導入] Component as API
React の力
- Component as 変換器 / Component as Pure Function
- 望ましいのは入力(Props)を受けて、出力(仮想DOM)する純粋関数
- class でなく function を使うべき、という話ではないです
- 制御側でうまく利用しやすいよう、Component を副作用のないAPIとして提供可能
// components/People.js
export default function People(props){
const { users } = props;
const items = users.length ? users.map(u => <li>{u.name}</li>) : <li>No Users</li>
return (
<ul>{ items }</ul>
);
}
// 利用の例
// 入力
const users = [ { name: "Taro" }, { name: "Jiro" }, ...];
const people = <People users={users} />;
// 出力 (イメージ)
<ul>
<li>Taro</li>
<li>Jiro</li>
...
</ul>
[導入] Component as API
Redux(Flux) の力
Flux的機構により多くのComponentをStatelessにすることが可能に
(後述するが、StatefulComponentをゼロにはできないと思う)
- Container Component & Presentational Component
- Presentational Component はUIのみに集中できる
// Container Component
const TodoListContainer = connect(
(storeState) => ({ todoList: storeState.todoList }),
(dispatch) => ({
onTodoComplete: (todoId) dispatch({ type: 'TODO_COMPLETE', payload: todoId })
})
)(TodoList)
// Presentational Component
function TodoList(props){
const { todoList } = props;
return (
<ul>...</ul>
);
}
[導入] Component as API
CSS-Modules & Webpack
- CSS-Modules: CSSにローカルスコープを(擬似的に)もたらす仕組み
- Webpack: JSファイルからCSSや画像ファイルを require/import できる仕組み
これらによって Component の利用方法(I/F仕様)がシンプルになる
- (Before)
<Hoge/>
を正しい見た目にするには、 hogeWrapper というクラスを付与して… - (After)
<Hoge/>
とさえ書けば Hoge 内部で閉じたCSS定義が正しい見た目にしてくれる
import * as styles from './TodoItem.scss';
function TodoItem(props){
return (
<div className={styles.todoItem} />
...
</div>
);
}
[本題] Component開発環境としての Storybook
「でも、アプリケーションに組み込まずにどうやって開発すんの???」
[本題] Component開発環境としての Storybook
React Storybook 概要
React Storybook is a UI development environment for your React components. With it, you can visualize different states of your UI components and develop them interactively.
It runs outside of your app. So you can develop UI components in isolation without worrying about app specific dependencies and requirements.
(https://getstorybook.io/docs)
公式ドキュメント: https://getstorybook.io/
※ React/Webpack 環境を前提のツールだと思われる
[本題] Component開発環境としての React Storybook
React Storybook 始め方 (詳しくは公式ドキュメントまで)
[Quick Start] (https://getstorybook.io/docs/basics/quick-start-guide)
npm i -g getstorybook
cd my-react-app
getstorybook
create-react-app で作ったプロジェクトなら簡単にいけるっぽいが、独自にビルド組んでる場合は?
Slow Start
自分がStorybook導入した時(v1.36.0)はこれしかなかった
npm i -D @kadira/storybook
- 設定ファイルをゴニョゴニョ書く
[本題] Component開発環境としての Storybook
React Storybook の基本的な使い方
まずコンポーネント自体の実装する (前述のPeopleと同じ)
// components/People.js
import React from 'react';
export default function People(props){
const { users } = props;
const items = users.length ? users.map(u => <li>{u.name}</li>) : <li>No Users</li>
return (
<ul>{ items }</ul>
);
}
Storyを実装する
// components/stories/People.js
import React from 'react';
import People from '../People';
import { storiesOf } from '@kadira/storybook';
storiesOf('People', module)
.add('with non-empty list', () => {
const users = [ { name: 'Taro' }, { name: 'Jiro' } ];
return (
<People users={users} />
);
});
[本題] Component開発環境としての Storybook
React Storybook の基本的な使い方
コマンド実行すると、何かビルドが走ってWebサーバー立ち上がる
$ npm run storybook
// package.json 内はこんな感じ
// "scripts": { "storybook": "start-storybook -p 9001" }
localhost:9001 にアクセスすると、Storybookが表示される
[本題] Component開発環境としての Storybook
React Storybook の基本的な使い方
当然ですが、Componentが受け取るPropsは色々ある。
... you can visualize different states of your UI components and develop them interactively.
https://getstorybook.io/docs
前述のPeopleコンポーネントの場合、空配列が渡された場合も動作確認したい。
// 前述の People.js から抜粋
...
const items = users.length ? users.map(u => <li>{u.name}</li>) : <li>No Users</li>
...
空配列の場合のStoryも追加する
// components/stories/People.js
storiesOf('People', module)
.add('with non-empty list', () => {
...
})
/* ここから追記 */
.add('with empty list', () => {
const users = [];
return (
<People users={users} />
);
});
Webpack の HMR のパワーで、コードを修正するとページのリフレッシュ無しに画面が更新される
[本題] Component開発環境としての Storybook
React Storybook の基本的な使い方
[本題] Component開発環境としての Storybook
Add-Ons
Storybook に追加機能をもたらすExtensionの機構。
デフォルトで linkTo/action などが提供されている。
[本題] Component開発環境としての Storybook
linkTo add-on
// components/stories/TodoItem.js
storiesOf('TodoItem')
.add('completed', () => (
<TodoItem onClick={linkTo('TodoItem', 'not completed')} />
))
.add('not completed', () => (
<TodoItem onClick={linkTo('TodoItem', 'completed')} />
));
ユーザのインタラクションに反応してStory間を遷移、状態遷移しているかのように見せられる。
[本題] Component開発環境としての React Storybook
action add-on
ユーザのインタラクションをLoggingしてくれる
add('xxx', () => (
<TodoFooter onClick={action('onClearCompleted')} />
));
[本題] Component開発環境としての React Storybook
Decorators
ファイル単位もしくは全ファイルで共通的に、Storyを囲う要素を差し込める。
- Storyをセンタリング表示する
- Storyの見た目が白系なので、わかりやすいように黒背景の上に載せるようにする
- Redux を動かす (後述)
const CenterDecorator = (story) => (
<div style={{ textAlign: "center" }}>
{story()}
</div>
);
storiesOf('Hoge', module)
.addDecorator(CenterDecorator)
.add('with text', () => (
<Hoge />
))
[本題] Component開発環境としての React Storybook
Head.html
head.html というファイルを定義しておくと、グローバルに適用したいCSSファイルやFontファイルをStobybook全体で読み込める。
[本題] Componentテスト環境としての Storybook
API提供するなら、ちゃんとテストされたものを提供したい!
でも、こういうやつは相当クリティカルなものじゃなきゃメンテナンスできる気がしない…
https://github.com/airbnb/enzyme[本題] Componentテスト環境としての Storybook
Storyshots で Structural Testing しよう
Snapshot testing is a way to test your UI component without writing actual test cases.
https://voice.kadira.io/snapshot-testing-in-react-storybook-43b3b71cec4f#.ndvrs9qmy
テストケース書かずに UI Component をテストできる?
[本題] Componentテスト環境としての Storybook
Structural Testing (Snapshot Testing) とは
With Snapshot testing, we keep a file copy of the structure of UI components. Think of it like a set of HTML sources.
Then, after we've completed any UI changes, we compare new snapshots with the snapshots that we kept in the file.
If things are not the same, we can do two things:
We can consider new snapshots that show the current state, and then update them as new snapshots.
We can find the root cause for the change and fix our code.
https://getstorybook.io/docs/testing/structural-testing
[本題] Componentテスト環境としての Storybook
Storyshots
- https://github.com/kadirahq/storyshots
- 内部的には Jest の Snapshot Testing 機能を利用している模様
- 2016年9月にリリースされたばかり!(この発表のネタ考えてる時に見つけた)
インストール
// Storybook の 2.17.0 以上が必要なので 古いVersionのStorybook をアップデートする
$ npm i -D @kadira/storybook@2.17.0
$ npm i -D @kadira/storyshots
package.json に script 追加
"scripts": {
"test-storybook": "storyshots"
}
[本題] Componentテスト環境としての Storybook
Storyshots の使い方
$ npm run test-storybook
Snapshotファイルが追加される。これを git commit
しておく。
[本題] Componentテスト環境としての Storybook
既存のComponentに変更が入ったら
[本題] Componentテスト環境としての Storybook
既存のComponentに変更が入ったら
再度 storyshots を実行
$ npm run test-storybook
前回までのSnapshotとの差分をチェックします。当然差分が検知され結果はエラーに。
修正対象のコンポーネント自体に加え、それに依存している他のコンポーネントのStoryが意図せず変更されても気づける。
気づいたら、実際にそのStoryを目視でチェックしにいけば良い。
[本題] Componentテスト環境としての Storybook
既存のComponentに変更が入ったら
全ての差分が問題ないことが確認できたら、Snapshot更新
$ npm run test-storybook -- -u
1Storyずつインタラクティブに判定していく場合はこちら
$ npm run test-storybook -- -i
(差分表示)
Update snapshot? (Y/n)
※ 通常の単体テストと異なりコマンド一つでテストをパスするようになるので、ノールックで更新しない最低限の倫理が必要。
もし、ローカル環境でのSnapshotの差分チェックを怠っていたら、Pull Requestをフックに自動テスト→エラーとすることも可能。
[本題] Componentテスト環境としての Storybook
実際のDOMのスナップショットを保持しているわけではないので注意
// .storybook/__snapshots__/TodoItem.snap
...
<label
onDoubleClick={[Function bound handleDoubleClick]}>
Hello Todo
</label>
<button
className="destroy"
onClick={[Function onClick]} />
...
出力されるVirtualDOMの構造が変わらなければ、CSS定義だけが変わってる場合などは気づけない。
CSS-Modulesなどをうまく利用し、そもそも副作用の少ないComponent開発を頑張る必要がある。
また、後述する CSS/Style Tesing も検討する。
[本題] Componentテスト環境としての Storybook
他のComponentテスト手法
Interaction Testing
Enzymeなどを利用し、実際にユーザがクリックしたりタイプして状態変化した結果までテストする
CSS/Style Testing
実際にDOMのレンダリングまで行った結果のスナップショットを保持し差分検知する。
StorybookではStoryごとにURLが発行される。
BackstopJSやGeminiなどのCSS/Style TesingツールにそのURLを渡すことで実現可能(らしい
http://localhost:9001/?selectedKind=People&selectedStory=with
[本題] ドキュメント生成ツールとしての Storybook
実装もテストもアプリケーションから切り離された環境でできるようになった
これでドキュメントも作れたら嬉しい
- Component API の利用者である ES君や他のW3C君向けの APIドキュメント
- 他のW3C君向けのスタイルガイド
- デザイナや企画職の人に確認してもらう成果物(特に開発環境が無い人達)
[本題] ドキュメント生成ツールとしての Storybook
Storybookを静的サイトにする
react-storybook が内包している build-storybook
コマンドを利用する
npm run
できるように package.json に追加
// c: 設定ファイルのディレクトリ o: 生成したファイルの出力先ディレクトリ
"scripts": {
"build-storybook": "build-storybook -c .storybook -o .out"
}
実行すると、 .out ディレクトリ配下に html/js ファイル等が出力される
$ npm run build-storybook
これをCIツールなど利用して、任意のサーバーにデプロイするようにしておけば、デザイナや企画の人など開発環境が無い人達でもさくっと確認可能
(手元でサクッと試したいなら、下記のような感じ)
$ cd .out
$ python -m SimpleHTTPServer
[本題] ドキュメント生成ツールとしての Storybook
ただ、APIドキュメントやスタイルガイドと呼ぶには不十分
[本題] ドキュメント生成ツールとしての Storybook
addon-infoでAPIドキュメントっぽくする
インストール方法は割愛(詳しくはリポジトリを参照)
addWithInfo メソッドを使って、第二引数部分に任意のテキストをMarkdownで記述する。
storiesOf('Header', module)
.addWithInfo(
'default view',
/* ↓↓↓↓↓↓↓↓↓↓↓↓ */
`
これはTodoアプリのHeaderですよ。
Props:
- addTodo: (todo: string) => void
利用例:
\`\`\`jsx
const addTodo = (todo) => dispatch({ type: 'ADD_TODO', payload: todo });
<Header addTodo={addTodo} />
\`\`\`
`,
/* ↑↑↑↑↑↑↑↑↑↑↑↑ */
() => {
return (
<div className="todoapp">
<Header addTodo={action('Add Todo')}/>
</div>
);
});
[本題] ドキュメント生成ツールとしての Storybook
addon-infoでAPIドキュメント化
Storybook上に入力したMarkdownが表示される。
さらに Story のソースコードや、PropTypesの定義を自動でドキュメントに載せてくれるので、
Componentの利用コード例を自分で書く手間が省けそう。
※ 自分で書くマークダウンに関しては、シンタックスハイライト未対応っぽい。
※ ドキュメント部分が前述のStoryshotsのSnapshotファイルに含まれてしまい test-storybook で差分検知されてしまう
[本題] ドキュメント生成ツールとしての Storybook
addon-notes
インストール方法は割愛(詳しくはリポジトリを参照)
WithNotes コンポーネントを利用して、ノートを記載。
import { WithNotes } from '@kadira/storybook-addon-notes';
...
return (
<WithNotes notes={'左側の丸をクリックしたら完了・非完了状態が切り替わるよ'}>
<div className="todoapp">
<div className="todo-list">
<TodoItem
todo={todo}
editTodo={action('editTodo')}
deleteTodo={action('deleteTodo')}
completeTodo={linkTo('TodoItem', linkOnClick)}/>
</div>
</div>
</WithNotes>
);
[本題] ドキュメント生成ツールとしての Storybook
その他ドキュメント系ツール
-
- GitHub Pages にコマンド一発でデプロイ可能(今後他のホスティングサービスにも対応予定?)
-
Storybook.io (有償サービス)
- Storybookのホスティング環境を提供
- Pull Request 毎に Storybook を自動デプロイ
- PRに対応するStorybook上にコメントなどして、レビュープロセスを効率化できるっぽい
気づけばStorybookの内容ばかりになってしまいました
(それぐらい、個人的に考えていたUI開発のあり方にドンピシャなソリューションを提供してくれてる)
活発に機能追加をやってるようなので最新情報をキャッチアップしたい方は:
- Newsletter 登録
- Slack Team に参加
[再掲] Storybook で Component開発の独立化
[脱線] Redux と Stateless Component
「本当に全て Stateless Component にすべきか? できるか?」
→ 無理ですし、バランス感が必要だと思う
→ isMenuOpen 程度の状態管理まで分業したくない
例えば下記のような場合は Component 側で完結したい:
- 選択中のタブとか、ちょっとした閉じ開きの状態
- フォームのバリデーション等、実際に触ってみないとイマイチ動作確認にならない
同時に、Reduxの "Single Source of Truth" の概念やタイムトラベルはなるだけ殺したくない。
[脱線] Redux と Stateless Component
Pseudo Local系のライブラリ
[脱線] Redux と Stateless Component
Redux-UI
選択中のタブとか、閉じ開きのフラグのようなComponentのLocalな状態で良さげなものに利用。
状態を持たない Presentational Component を実現しつつ、reducer をイチイチ記述必要が無い。(内部的に redux-ui が state管理してくれる)
// containers/ToggleParagraphContainer.js
import ui from 'redux-ui';
const ToggleParagraphContainer = ui({
state: { isOpen: true }
})(ToggleParagraph)
// components/ToggleParagraph.js
function ToggleParagraph({ ui, updateUI }){
const onClick = () => updateUI({ isOpen: true })
return (
<div>
<button>{ui.isOpen ? 'Close' : 'Open' }</button>
{ ui.isOpen && <p>Storybook 最高!</p> }
</div>
);
}
[脱線] Redux と Stateless Component
Redux-Form
基本的に redux-ui と同じような発想。
Form の入力やバリデーションに特化している。
import form, {Field} from 'redux-form';
const UserEditFormContainer = form({
form: 'userEditForm',
initialValues: { userName: 'kitaly' },
validate: validateFn,
onSubmit: onSubmitFn
})()
function UserEditForm(props){
return (
<form>
<Field name="userName" component={UserNameInput} />
<input type="submit" value="更新"/>
</form>
);
}
function UserNameInput(props){
return (
<p>
<input type="text" {...props.input} />
{props.meta.error && <p>{props.meta.error}</p>
</p>
);
}
[脱線] Redux と Stateless Component
Redux-UI/Redux-Form と Storybook
これで状態管理をReduxに任せつつ、コンポーネント開発側だけで完結できる。
// .storybook/config.js
import {reducer as uiReducer } from 'redux-ui';
setAddon({
addWithRedux(storyName, storyFn) {
const store = createStore(
combineReducers({
form: formReducer,
ui: uiReducer
}),
{}
);
this.add(storyName, (context) => (
<Provider store={store}>
{storyFn(context)}
</Provider>
));
}
});
// containers/stories/DropdownMenuContainer.js
storiesOf('DropdownMenuContainer', module)
.addWithRedux('normal', () => (
<DropdownMenuContainer />
));
[脱線] Redux と Stateless Component
Pseudo Local 系ライブラリの注意
不用意に汎用的な Component まで Redux依存になっていくのも好ましくない
→ 適宜 React標準の setState を利用するなどバランス間を持って使い分ける
また Stateful な Storyは、前述の Structural Testing ではカバーしきれなくなる
→ Container Component のStoryで目視で動作確認しつつ、Presentational Component についても色々なケースのStoryを作る
→ Enzymeなどを利用して Interaction Testing を実施する
[まとめ] 感想
- Storybook 素敵すぎる
- 気づいたらほとんどStorybookのドキュメントで書いてあることばかりになった…orz
- 苦でない人は公式ドキュメントやDemoプロジェクトを見ましょう
- 分業とは言ったものの、この体制をつくるためにES君とW3C君のハイブリッド人材は1人は必要かも
- W3C君はAPIを使う側から提供する側へ、I/F設計やテスト等を考える必要性、やはり変化無しにできるわけではない
- もちろんES君もCSS勉強しましょ