こちらの要約。流石に長過ぎるので、キリの良い所で分割する。
A Comprehensive Guide to Test-First Development with Redux, React, and Immutable
Reduxは怖くない。ReduxはとてもシンプルなライブラリなのでAPIをすべて学ぶことは難しいことではない。
このチュートリアルでは、full-stackのReduxとImmutable.jsをscrachからビルドする。アプリは、Node+ReduxバックエンドとReact+Reduxフロントエンドで構築し、テストファースト開発を行うことにしよう。
The App
会議等で使える投票アプリを開発する。アプリの中身としては、アイテムごとにペアを作り、各ペアごとに投票、勝者がさらに上位のペアを作り、アイテムが残り1つになるまで、投票を続ける。つまりトーナメント形式。
アプリは、モバイルデバイスやウェブブラウザに使用される投票UIと、より大きなスクリーン用のリアルタイムで投票結果を反映する結果UIの、2つのUIを持つ。
The Architecture
システムは2つのアプリから構成される: 2つのUIを提供するブラウザアプリと、投票ロジックを扱うサーバーアプリ。これら2つの間はWebSocketsが取り持ってくれる。
クライアントとサーバーのアプリ両方を Redux を使って構成する。stateの保持には Immutable データ構造を使用する。
The Server Application
まずはNode(サーバー)アプリを書いてからReact(クライアント)アプリを書く。こうすればUIについて考え始める前に、コアロジックにのみ集中できる。
Designing The Application State Tree
Reduxアプリの設計はstateデータ構造を考えることから始まる。
すべてのフレームワーク、アーキテクチャはstateを持つ。EmberやBackBoneではstateはModelに。AngularではFactoryやServiceに。ほとんどのFluxフレームワークではStoreに。では、Reduxはどうだろう?
大きな違いは、Reduxでは、stateはひとつの木構造に保存される。
投票アプリではどうなるか見てみよう。最初のstateは投票するアイテムの集合 entries
とする。
一度投票が始まれば、投票中のアイテムは区別されるべきだろう。このstateでは投票中のペアを保持するvoteを作る。
投票開始後には、それぞれ投票数を保存する。
投票終了後、負けたアイテムは消滅。勝ったアイテムは最後のアイテムとしてentriesに戻る。このアイテムは後に別のペアを作って再度投票される。次の2つのentriesが投票される。
アイテムが最後の一つになったら投票を終わって、それをwinnerとして宣言する。
Project Setup
さて、実際に手を動かそう。まずはプロジェクトのセットアップ。
mkdir voting-server cd voting-server npm init -y
ES6でコードを書くのでトランスパイル用のBabelを入れる。
npm install --save-dev babel-core babel-cli babel-preset-es2015
テスト用のライブラリもインストール。
npm install --save-dev mocha chai
Mochaのテストコマンドを設定しておこう。
package.json
"scripts": { "test": "mocha --compilers js:babel-core/register --recursive", "test:watch": "npm run test -- --watch" },
Babelもpackage.json
に設定。
"babel": { "presets": ["es2015"] }
これでテストコマンドが実行可能になる。
npm run test
データ構造を提供してくれる Immutable とそれ用の chai-immutable をインストールしておく。
npm install --save immutable npm install --save-dev chai-immutable
chai-immutableを使うようにtest_helper.js
ファイルを作成する。
import chai from 'chai'; import chaiImmutable from 'chai-immutable'; chai.use(chaiImmutable);
テストコマンドをこのファイルを使用するように編集する。
"scripts": { "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive", "test:watch": "npm run test -- --watch" },
Getting Comfortable With Immutable
Reduxアプリのstateツリーはimmutable data structure(不変のデータ構造)で、別のstateツリーを生成することで次のstateに移行する。
つまり2つの連続的なstateは2つの独立したツリーに格納されている。現在のstateを引数に取り、新しいstateを返す関数を使用することで、stateツリー間の移動を行う。
stateの変更前、変更後の状態が保存されているのでredo/undo(やり直し/取り消し)が容易になる。
それだけでなく、コードもシンプルなものになる。stateの変更にはデータを引数に取り、データを返すピュア関数を使うので、関数の動作や変更後のstateが予測できる。
immutability(データの不変性)を理解するために、最もシンプルなデータ構造である"カウンター"を考えてみよう。stateは0から1,2,3...と増えていく。
カウンターがインクリメント(増加)したとき、数字自体は変更しない。ピュア関数を用いて現在のstateや数字を変更するのではなく、次のstateを取得する。この関数のテストを考えてみよう。
test/immutable_spec.js
import { expect } from 'chai'; describe('immutability', () => { describe('a number', () => { function increment(currentState) { return currentState + 1; } it('is immutable', () => { let state = 42; let nextState = increment(state); expect(nextState).to.equal(43); expect(state).to.equal(42); }); }); });
新しいstateが生成されているが、stateに変更が加えられているわけではない。
このデータ構造の考えを発展させよう。
Immutable Listを用いた、stateが映画のリストであるようなアプリを考えてみる。映画を加えると、古いリストとその映画を合わせた、新しいリストを生成する。
test/immutable_spec.js
import { expect } from 'chai'; import { List } from 'immutable'; // ... describe('A List', () => { function addMovie(currentState, movie) { return currentState.push(movie); } it('is immutable', () => { let state = List.of('Trainspotting', '28 Days Later'); let nextState = addMovie(state, 'Sunshine'); expect(nextState).to.equal(List.of( 'Trainspotting', '28 Days Later', 'Sunshine' )); expect(state).to.equal(List.of( 'Trainspotting', '28 Days Later' )); }); }); });
配列にpushしても、古いstateは変わらずに存在している。この考え方はstate tree にも応用される。state treeはList, Map 等の集合の入れ子になったデータ構造にすぎない。何かの操作をすることで新しいstate treeを生成するが、以前のstate treeには全く変更を加えない。state treeが映画のリストを含む movies
をkeyとするMapであるなら、映画を加えるということは、新しいMapを作り出す必要がある。
test/immutable_spec.js
import {expect} from 'chai'; import {List, Map} from 'immutable'; describe('immutability', () => { // ... describe('a tree', () => { function addMovie(currentState, movie) { return currentState.set( 'movies', currentState.get('movies').push(movie) ); } it('is immutable', () => { let state = Map({ movies: List.of('Trainspotting', '28 Days Later') }); let nextState = addMovie(state, 'Sunshine'); expect(nextState).to.equal(Map({ movies: List.of( 'Trainspotting', '28 Days Later', 'Sunshine' ) })); expect(state).to.equal(Map({ movies: List.of( 'Trainspotting', '28 Days Later' ) })); }); }); });
これは前のものと全く同じで、ネストされたデータ構造にも応用される。
Immutableはこうしたネストされたデータに到達し、値を更新するために、いくつかの関数を提供している。ここでは、updateを使用する。
test/immutable_spec.js
function addMovie(currentState, movie) { return currentState.update('movies', movies => movies.push(movie)); }
次回: 第二回 投票アプリケーションのロジック