Reduxガイド

今日、 ReduxはJavaScriptの世界で最も興味深い現象の1つです。 数百のライブラリとフレームワークから際立っており、シンプルで予測可能な状態モデルを導入し、関数型プログラミングと不変データに逸脱し、コンパクトなAPIを提供することで、さまざまな問題を有能に解決します。 幸せには他に何が必要ですか? Redux-ライブラリは非常に小さく、APIの学習は難しくありません。 しかし、多くの人々にとって、一種のテンプレートブレークが発生します。少数のコンポーネントと、純粋な関数と不変のデータに対する自発的な制限は、不当な強制に見えるかもしれません。 そのような状況でどのように正確に働くのですか?



このチュートリアルでは、ReduxとImmutable-jsを使用してフルスタックアプリケーションをゼロから作成する方法を説明します。 TDDアプローチを適用して、Node + ReduxバックエンドおよびReact + Reduxフロントエンドアプリケーションを構築するすべての段階を実行します。 さらに、ES6、 BabelSocket.ioWebpackMochaなどのツールを使用します。 セットは非常に好奇心is盛で、すぐにマスターできます!



記事の内容



1.必要なもの

2.アプリケーション

3.アーキテクチャ

4.サーバーアプリケーション

4.1。 アプリケーション状態ツリーを開発する

4.2。 プロジェクトのセットアップ

4.3。 不変データの紹介

4.4。 純粋な関数を使用したアプリケーションロジックの実装

4.4.1。 レコードをダウンロードする

4.4.2。 投票開始

4.4.3。 投票

4.4.4。 次のペアに行く

4.4.5。 投票の完了

4.5。 アクションとレデューサーの使用

4.6。 味覚抑制剤組成

4.7。 Reduxストアの使用

4.8。 Socket.ioサーバーを構成する

4.9。 Reduxリスナーからのブロードキャストステータス

4.10。 Reduxリモートアクションの取得

5.クライアントアプリケーション

5.1。 クライアントプロジェクトのセットアップ

5.1.1。 単体テストのサポート

5.2。 反応および反応ホットローダー

5.3。 投票画面のインターフェースを作成する

5.4。 不変データと純粋なレンダリング

5.5。 結果画面のインターフェースの作成とルーティングの処理

5.6。 クライアントRedux-Storeの使用

5.7。 ReduxからReactに入力を渡す

5.8。 Socket.ioクライアントの構成

5.9。 サーバーからアクションを取得する

5.10。 Reactコンポーネントからアクションを渡す

5.11。 Reduxミドルウェアを使用してサーバーにアクションを送信する

6.演習



1.必要なもの



このガイドは、JavaScriptアプリケーションの作成方法をすでに知っている開発者に最も役立ちます。 すでに述べたように、Node、ES6、 ReactWebpackBabelを使用します。これらのツールに少しでも精通していれば、プロモーションに問題はありません。 慣れていない場合でも、途中で基本を理解できます。



React、Webpack、ES6を使用してWebアプリケーションを開発するための良いガイドとして、SurviveJSをお勧めします 。 ツールに関しては、NPM備えたNodeとお好みのテキストエディターが必要です。



2.アプリケーション



パーティー、会議、会議、その他の会議でのライブ投票の申し込みを行います。 アイデアは、ユーザーに投票ポジションのコレクションが提供されるということです:映画、歌、プログラミング言語、 Horse JSからの引用など。 アプリケーションは、すべての人がお気に入りに投票できるように、要素をペアで配置します。 一連の投票の結果、1つの要素(勝者)が残ります。 ダニー・ボイルの最高の映画に投票する例:







アプリケーションには、2つの異なるユーザーインターフェイスがあります。









3.アーキテクチャ



構造的に、システムは2つのアプリケーションで構成されます。





アプリケーション間の相互作用は、WebSocketを使用して実行されます。 Reduxは、クライアントとサーバーのコードを整理するのに役立ちます。 そして、状態を保存するために、 不変の構造を使用します。



クライアントとサーバーの大きな類似性にもかかわらず-たとえば、両方がReduxを使用します-これは汎用/同形アプリケーションではなく、アプリケーションはコードを共有しません。 むしろ、メッセージングを介して相互に作用する2つのアプリケーションの分散システムとして説明できます。



4.サーバーアプリケーション



最初にNodeアプリケーションを作成し、次にReactを作成します。 これにより、インターフェイスに移る前に、基本的なアプリケーションロジックの実装に気を取られないようになります。 サーバーアプリケーションを作成しているので、ReduxとImmutableに精通し、それらに基づいて構築されたアプリケーションがどのように配置されるかを調べます。 Reduxは通常Reactプロジェクトに関連付けられていますが、その使用はそれらに限定されません。 特に、Reduxが他のコンテキストでどのように役立つかを調べます!



このガイドを読みながら、アプリケーションをゼロから作成することをお勧めしますが、 GitHubからソースをダウンロードできます。



4.1。 アプリケーション状態ツリーを開発する



Reduxを使用したアプリケーションの作成は、多くの場合、アプリケーション状態データ構造の構造を検討することから始まります 。 その助けを借りて、アプリケーションのあらゆる瞬間に何が起こるかを説明します。 状態とアーキテクチャには状態があります。 EmberおよびBackboneに基づくアプリケーションでは、状態はモデルに保存されます。 Angularベースのアプリケーションでは、状態はほとんどの場合ファクトリーとサービスに保存されます。 ほとんどのFluxアプリケーションでは、状態はストア(ストア)です。 これはReduxでどのように行われますか?



主な違いは、すべてのアプリケーションの状態が単一のツリー構造に保存されることです。 したがって、アプリケーションの状態について知る必要があるものはすべて、連想配列(マップ)と通常の配列の1つのデータ構造に含まれています。 すぐにわかるように、この決定には多くの結果があります。 最も重要なことの1つは、アプリケーションの状態と動作を分離できることです。 状態は純粋なデータです。 メソッドや関数は含まれておらず、他のオブジェクト内に隠されていません。 すべてが1か所にあります。 これは、特にオブジェクト指向プログラミングの経験がある場合、制限のように思えるかもしれません。 しかし、これは実際にはより大きな自由の現れです。なぜなら、データだけに集中できるからです。 十分な時間を割くと、アプリケーションの状態の設計から多くのことが論理的に流れます。



最初に常に状態ツリーを完全に開発し、次に残りのアプリケーションコンポーネントを作成する必要があるとは言いたくありません。 これは通常、並行して行われます。 しかし、コードを書き始める前に、さまざまな状況でツリーがどのように見えるべきかを最初に一般的に概説する方がより便利であるように思えます。 投票アプリケーションに状態ツリーがどのようになるか想像してみましょう。 アプリケーションの目的は、オブジェクトのペア(映画、音楽グループ)の中で投票できるようにすることです。 アプリケーションの初期状態として、投票に参加するポジションのコレクションを作成することをお勧めします。 このエントリのコレクションを呼び出します







投票の開始後、現在投票に参加しているポジションを何らかの形で分ける必要があります。 状態は、ユーザーがどちらかを選択する必要がある位置のペアを含む投票エンティティである場合があります。 当然、このペアはエントリコレクションから抽出する必要があります







また、投票結果の記録を保持する必要があります。 これは、 vote内の別の構造を使用して実行できます。







現在の投票の終わりに、負けたエントリーは破棄され、勝ったエントリーはエントリーに戻され、リストの最後に置かれます 。 後で、彼女は再び投票します。 次に、リストから次のペアが取得されます。







コレクションにエントリがある限り、これらの状態は循環的に相互に置き換えられます。 最終的に、勝者として宣言されたエントリは1つだけになり、投票は終了します。







スキームは非常に合理的であると思われるので、実装を始めましょう。 これらの要件の状態を開発するにはさまざまな方法がありますが、おそらくこのオプションは最適ではありません。 しかし、これは特に重要ではありません。 最初のアウトラインは、始めるのにちょうど良いはずです。 主なことは、アプリケーションがどのように機能するかを理解していることです。 そして、これはコードを書き始める前です!



4.2。 プロジェクトのセットアップ



袖をまくりましょう。 最初にプロジェクトフォルダーを作成し、それをNPMプロジェクトとして初期化する必要があります。



mkdir voting-server cd voting-server npm init -y
      
      





作成されたフォルダーには、とりあえず、唯一のpackage.json



ファイルがあります。 ES6仕様でコードを記述します。 Nodeはバージョン4.0.0以降、多くのES6機能をサポートしていますが、必要なモジュールはまだ残っています。 したがって、ES6の全機能を使用してコードをES5に変換できるように、Babelをプロジェクトに追加する必要があります。



 npm install --save-dev babel-core babel-cli babel-preset-es2015
      
      





単体テストを作成するためのライブラリも必要になります。



 npm install --save-dev mocha chai
      
      





Mochaをテストフレームワークとして使用します。 テストの中で、 Chaiをライブラリとして使用して、予想される動作と条件をテストします。 mocha



を使用してテストを実行します。



 ./node_modules/mocha/bin/mocha --compilers js:babel-core/register --recursive
      
      





その後、Mochaはすべてのプロジェクトテストを再帰的に検索して実行します。 Babelは、ES6コードを起動する前にトランスパイルするために使用されます。 便宜上、このコマンドをpackage.json



保存できます。



 package.json "scripts": { "test": "mocha --compilers js:babel-core/register --recursive" },
      
      





次に、BabelでES6 / ES2015のサポートを含める必要があります。 これを行うには、既にインストールされているパッケージbabel-preset-es2015



ます。 次に、 "babel"



セクションをpackage.json



追加します。



 package.json "babel": { "presets": ["es2015"] }
      
      





npm



コマンドを使用して、テストを実行できます。



 npm run test
      
      





test:watch



を使用して、コードの変更を追跡し、各変更後にテストを実行するプロセスを開始できます。



 package.json "scripts": { "test": "mocha --compilers js:babel-core/register --recursive", "test:watch": "npm run test -- --watch" },
      
      





Facebookが開発した不変ライブラリは、多くの有用なデータ構造を提供します。 これについては次の章で説明しますが、とりあえず、不変の構造をChaiと比較するためのサポートを追加するchai不変ライブラリとともにプロジェクトに追加します。



 npm install --save immutable npm install --save-dev chai-immutable
      
      





テストを実行する前に、chai-immutableを接続する必要があります。 test_helper



ファイルを使用してこれを行うことができます。



 test/test_helper.js import chai from 'chai'; import chaiImmutable from 'chai-immutable'; chai.use(chaiImmutable);
      
      





テストを実行する前に、Mochaにこのファイルをロードさせます。



 package.json "scripts": { "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive", "test:watch": "npm run test -- --watch" },
      
      





これで、すべてを開始できます。



4.3。 不変データの紹介



Reduxのアーキテクチャに関連する2番目の重要な点:状態は単なるツリーではなく、 不変のツリー(不変のツリー)です。 前の章のツリーの構造は、連想配列内の要素の置換、配列からの削除など、ツリーを更新するだけでコードがアプリケーションの状態を変更することを示唆している場合があります。 しかし、Reduxでは、すべてが異なる方法で行われます。 Reduxアプリケーションの状態ツリーは、 不変のデータ構造です。 これは、ツリーが存在している間は変更されないことを意味します。 常に同じ状態を維持します。 また、別の状態への遷移は、必要な変更が加えられた別のツリーを作成することにより実行されます。 つまり、2つの連続したアプリケーション状態が2つの独立した独立したツリーに格納されます。 また、ツリー間の切り替えは、現在の状態を取得して次の状態を返す 関数を呼び出すことによって行われます







これはいいアイデアですか? 通常、すべての状態が1つのツリーに保存され、これらすべての安全な更新を行うと、アプリケーションの状態の履歴を簡単に保存できることがすぐに示されます。 これにより、「無料」で元に戻す/やり直しを実装できます。履歴から前または次の状態(ツリー)を設定するだけです。 また、ストーリーをシリアル化して将来のために保存したり、後で再生するためにリポジトリに保存したりすることもできます。これはデバッグに非常に役立ちます。



しかし、これらすべての追加機能に加えて、不変データを使用する主な利点はコードの簡素化であるように思えます。 純粋な関数をプログラムする必要があります 。それらはデータの受け取りと返しのみを行い、それ以上は行いません。 これらの関数は予測どおりに動作します。 何度でも呼び出すことができ、常に同じように動作します。 それらに同じ引数を与えると、同じ結果が得られます。 関数を呼び出すための「ユニバースを準備する」ためにスタブやその他の偽物を設定する必要がないため、テストは簡単になります。 入力と出力だけがあります。



不変の構造を使用してアプリケーションの状態を説明するので、作業を説明するためにいくつかの単体テストを作成して、それらを知るのに時間をかけましょう。



不変データと不変ライブラリを自信を持って使用している場合は、次のセクションに進むことができます。



不変性の概念を理解するために、まず最も単純なデータ構造について話をすることができます。 状態が数値であるカウンターアプリケーションがあるとします。 0から1、2、3のように変化するとします。 基本的に、数字はすでに不変のデータと考えています。 カウンターが増加しても、数値は変化しません。 はい、数字には「セッター」がないため、これは不可能です。 42.setValue(43)



と言うことはできません。



したがって、前の番号に1を追加して、 異なる番号を取得するだけです。 これは、純粋な関数を使用して実行できます。 彼女の引数は現在の状態になり、戻り値は次の状態として使用されます。 呼び出された関数は、現在の状態を変更しません。 彼女の例と彼女の単体テストは次のとおりです。



 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); }); }); });
      
      





数値は不変なので、 increment



呼び出されてもstate



は変化しません。



ご覧のとおり、このテストはアプリケーションでは何も行いません。まだ作成していません。



テストは私たちにとって単なる学習ツールになります。 いくつかのアイデアを実行する単体テストを作成することで、新しいAPIやテクニックを学ぶことはしばしば便利です。 本「 テスト駆動開発」では、このようなテストは「トレーニングテスト」と呼ばれます。



ここで、不変性の概念を、数字だけでなく、あらゆる種類のデータ構造に拡張します。



不変リストを使用して、たとえば、状態が映画のリストであるアプリケーションを作成できます。 新しいムービーを追加する操作により、 新しいリストが作成されます。 これは、古いリストと追加する位置の組み合わせです 。 この操作の後、古い状態は変わらないことに注意することが重要です:



 test/immutable_spec.js import {expect} from 'chai'; import {List} from 'immutable'; describe('immutability', () => { // ... 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' )); }); }); });
      
      





そして、ムービーを通常の配列に挿入すると、古い状態が変更されます。 ただし、代わりにImmutableのリストを使用するため、前の例の数字と同じセマンティクスを使用します。



通常の配列に貼り付けると、古い状態が変わります。 しかし、不変リストを使用しているため、数値の例と同じセマンティクスを持っています。



この考え方は、本格的な状態ツリーにも当てはまります。 ツリーは、リスト、連想配列( マップ )、およびその他のタイプのコレクションのネスト構造です。 これに適用される操作により、 新しい状態ツリーが作成され、前のツリーは変更されません。 ツリーがムービーのリストを含むムービーキーを持つ連想配列である場合、新しい位置を追加すると、ムービーキーが新しいリストを指す新しい配列を作成する必要があります。



 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には、更新された値を取得するためにネストされたデータに簡単に「アクセス」できるようにするいくつかの補助関数があります。 簡潔にするために、 更新機能を使用できます。



 test/immutable_spec.js function addMovie(currentState, movie) { return currentState.update('movies', movies => movies.push(movie)); }
      
      





アプリケーションで同様の関数を使用して、アプリケーションの状態を更新します。 Immutable APIは他の多くの機能を隠しているので、氷山の一角に注目しました。



不変データはReduxアーキテクチャの重要な側面ですが、不変ライブラリを使用するための厳密な要件はありません。 Redux公式ドキュメントでは、大部分が単純なJavaScriptオブジェクトと配列に言及しており、慣例により変更することは控えています。



不変ライブラリがマニュアルで使用される理由はいくつかあります。





4.4。 純粋な関数を使用したアプリケーションロジックの実装



不変の状態ツリーとこれらのツリーで動作する関数の概念を理解したら、アプリケーションのロジックの作成に進むことができます。 これは、上記で説明したコンポーネント、つまりツリー構造と、このツリーの新しいバージョンを作成する一連の関数に基づいています。



4.4.1。 レコードをダウンロードする



まず、アプリケーションは投票エントリのコレクションを「ダウンロード」する必要があります。 setEntries



関数setEntries



以前の状態とコレクションをsetEntries



そこにレコードを含めることで新しい状態を作成できます。 この関数のテストは次のとおりです。



 test/core_spec.js import {List, Map} from 'immutable'; import {expect} from 'chai'; import {setEntries} from '../src/core'; describe('application logic', () => { describe('setEntries', () => { it('   ', () => { const state = Map(); const entries = List.of('Trainspotting', '28 Days Later'); const nextState = setEntries(state, entries); expect(nextState).to.equal(Map({ entries: List.of('Trainspotting', '28 Days Later') })); }); }); });
      
      





setEntries



の初期実装では、最も単純な処理のみが行われます。状態連想配列のentries



キーには、指定されたエントリのリストが値として割り当てられます。 以前に設計された最初のツリーを取得します。



 src/core.js export function setEntries(state, entries) { return state.set('entries', entries); }
      
      





便宜上、入力エントリを通常のJavaScript配列(または反復可能なもの)にします。 状態ツリーには、不変リスト( List



)が存在する必要があります。



 test/core_spec.js it('  immutable', () => { const state = Map(); const entries = ['Trainspotting', '28 Days Later']; const nextState = setEntries(state, entries); expect(nextState).to.equal(Map({ entries: List.of('Trainspotting', '28 Days Later') })); });
      
      





この要件を満たすために、リストコンストラクターにエントリを転送します。



 src/core.js import {List} from 'immutable'; export function setEntries(state, entries) { return state.set('entries', List(entries)); }
      
      





4.4.2。 投票開始



すでにレコードのセットがある状態でnext



関数を呼び出すことにより、投票を開始できます。 したがって、設計されたツリーの最初のツリーから2番目のツリーへの遷移が実行されます。



この関数には追加の引数は必要ありません。 それはvote



連想配列を作成するべきです、最初の2つのエントリーがキーペアにあります。 同時に、現在投票に参加しているentries



は、 entries



リストに含まれなくなります。



 test/core_spec.js import {List, Map} from 'immutable'; import {expect} from 'chai'; import {setEntries, next} from '../src/core'; describe(' ', () => { // .. describe('', () => { it('     ', () => { const state = Map({ entries: List.of('Trainspotting', '28 Days Later', 'Sunshine') }); const nextState = next(state); expect(nextState).to.equal(Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later') }), entries: List.of('Sunshine') })); }); }); });
      
      





関数の実装は、更新を古い状態と結合(マージ)し 、最初のエントリを個別のリストから分離し、残りをentries



リストの新しいバージョンに分離しentries







 src/core.js import {List, Map} from 'immutable'; // ... export function next(state) { const entries = state.get('entries'); return state.merge({ vote: Map({pair: entries.take(2)}), entries: entries.skip(2) }); }
      
      





4.4.3。 投票



投票が続くと、ユーザーはさまざまなエントリに投票することができます。 そして、新しい投票ごとに、現在の結果が画面に表示されるはずです。 特定のエントリが既に投票されている場合、そのカウンタは増加するはずです。



 test/core_spec.js import {List, Map} from 'immutable'; import {expect} from 'chai'; import {setEntries, next, vote} from '../src/core'; describe(' ', () => { // ... describe('vote', () => { it('     ', () => { const state = Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later') }), entries: List() }); const nextState = vote(state, 'Trainspotting'); expect(nextState).to.equal(Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 1 }) }), entries: List() })); }); it('       ', () => { const state = Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 3, '28 Days Later': 2 }) }), entries: List() }); const nextState = vote(state, 'Trainspotting'); expect(nextState).to.equal(Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 4, '28 Days Later': 2 }) }), entries: List() })); }); }); });
      
      





ImmutableのfromJS関数を使用すると、これらすべてのネストされたスキーマとリストをより簡潔に作成できます。



テストを実行します。



 src/core.js export function vote(state, entry) { return state.updateIn( ['vote', 'tally', entry], 0, tally => tally + 1 ); }
      
      





updateInを使用すると、ツリーに思考を広めないようにすることができます。 このコードは、「ネストされたデータ構造[ 'vote'



'tally'



'Trainspotting'



]のパスを取得し、この関数を適用します。 欠落しているキーがある場合は、代わりに新しい配列( Map



)を作成します。 最後に値がない場合は、ゼロで初期化してください。 不変のデータ構造での作業を楽しむことができるのはこの種のコードなので、時間をかけて練習する必要があります。



4.4.4。 次のペアに行く



現在のペアの投票が終了したら、次のペアに進みます。 勝者を保存し、エントリのリストの最後に追加して、後で再び投票に参加できるようにする必要があります。 負けた記録は単に捨てられます。 同点の場合、両方のエントリが保持されます。



next



の既存next



実装にこのロジックを追加します。



 test/core_spec.js describe('next', () => { // ... it('       ', () => { const state = Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 4, '28 Days Later': 2 }) }), entries: List.of('Sunshine', 'Millions', '127 Hours') }); const nextState = next(state); expect(nextState).to.equal(Map({ vote: Map({ pair: List.of('Sunshine', 'Millions') }), entries: List.of('127 Hours', 'Trainspotting') })); }); it('        ', () => { const state = Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 3, '28 Days Later': 3 }) }), entries: List.of('Sunshine', 'Millions', '127 Hours') }); const nextState = next(state); expect(nextState).to.equal(Map({ vote: Map({ pair: List.of('Sunshine', 'Millions') }), entries: List.of('127 Hours', 'Trainspotting', '28 Days Later') })); }); });
      
      





実装では、現在の投票の勝者とエントリーを単純に結び付けます。 そして、新しいgetWinners



関数を使用してこれらの勝者を見つけることができます。



 src/core.js function getWinners(vote) { if (!vote) return []; const [a, b] = vote.get('pair'); const aVotes = vote.getIn(['tally', a], 0); const bVotes = vote.getIn(['tally', b], 0); if (aVotes > bVotes) return [a]; else if (aVotes < bVotes) return [b]; else return [a, b]; } export function next(state) { const entries = state.get('entries') .concat(getWinners(state.get('vote'))); return state.merge({ vote: Map({pair: entries.take(2)}), entries: entries.skip(2) }); }
      
      





4.4.5。 投票の完了



ある時点で、記録が1つしか残っていません-勝者、そして投票が終了します。 そして、新しい投票を生成する代わりに、このエントリを現在の状態の勝者として明示的に指定します。 投票の終わり。



 test/core_spec.js describe('next', () => { // ... it('    ,    ', () => { const state = Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 4, '28 Days Later': 2 }) }), entries: List() }); const nextState = next(state); expect(nextState).to.equal(Map({ winner: 'Trainspotting' })); }); });
      
      





next



実装では、次の投票の完了後、エントリのリストに位置が1つしか残っていない場合に、状況の処理を提供する必要があります。



 src/core.js export function next(state) { const entries = state.get('entries') .concat(getWinners(state.get('vote'))); if (entries.size === 1) { return state.remove('vote') .remove('entries') .set('winner', entries.first()); } else { return state.merge({ vote: Map({pair: entries.take(2)}), entries: entries.skip(2) }); } }
      
      





ここでは単にMap({winner: entries.first()})



返すことができます。 ただし、代わりに、古い状態を再び使用して、 vote



およびentries



キーを明示的に削除しentries



。 これは将来を見据えて行われます。現在の状態では、この関数を使用して変更せずに転送する必要があるサードパーティのデータが表示されることがあります。 一般に、状態変換の機能の基礎は良い考えです-新しい状態をゼロから作成するのではなく、常に古い状態を新しい状態に変換します。



これで、アプリケーションのメインロジックの完全に受け入れ可能なバージョンが作成され、いくつかの関数として表されました。 ユニットテストも作成しましたが、これは非常に簡単に提供されました。プリセットやスタブはありません。 これは、純粋な機能の美しさの現れです。 単純に呼び出して、戻り値を確認できます。



Reduxもまだインストールしていないことに注意してください。 同時に、彼らはこのタスクに「フレームワーク」を関与させることなく、アプリケーションロジックの開発に冷静に従事していました。 それについて気の利いた何かがあります。



4.5。 アクションとレデューサーの使用



したがって、主要な機能はありますが、Reduxで直接呼び出すことはありません。 関数と外部の世界の間には、間接的なアドレッシングの層があります: Actions







これらは、アプリケーションの状態で発生する必要がある変更を記述する単純なデータ構造です。 本質的に、これは小さなオブジェクトにパッケージ化された関数呼び出しの説明です。 , type



, , . . , :



 {type: 'SET_ENTRIES', entries: ['Trainspotting', '28 Days Later']} {type: 'NEXT'} {type: 'VOTE', entry: 'Trainspotting'}
      
      





. VOTE



:



 //  action let voteAction = {type: 'VOTE', entry: 'Trainspotting'} //   : return vote(state, voteAction.entry);
      
      





(generic function), — — . ( reducer



):



 src/reducer.js export default function reducer(state, action) { // ,    ,    }
      
      





, reducer :



 test/reducer_spec.js import {Map, fromJS} from 'immutable'; import {expect} from 'chai'; import reducer from '../src/reducer'; describe('reducer', () => { it('handles SET_ENTRIES', () => { const initialState = Map(); const action = {type: 'SET_ENTRIES', entries: ['Trainspotting']}; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ entries: ['Trainspotting'] })); }); it('handles NEXT', () => { const initialState = fromJS({ entries: ['Trainspotting', '28 Days Later'] }); const action = {type: 'NEXT'}; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'] }, entries: [] })); }); it('handles VOTE', () => { const initialState = fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'] }, entries: [] }); const action = {type: 'VOTE', entry: 'Trainspotting'}; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} }, entries: [] })); }); });
      
      





reducer . , :



 src/reducer.js import {setEntries, next, vote} from './core'; export default function reducer(state, action) { switch (action.type) { case 'SET_ENTRIES': return setEntries(state, action.entries); case 'NEXT': return next(state); case 'VOTE': return vote(state, action.entry) } return state; }
      
      





, reducer , .



reducer- : , , . . , undefined



, :



 test/reducer_spec.js describe('reducer', () => { // ... it('has an initial state', () => { const action = {type: 'SET_ENTRIES', entries: ['Trainspotting']}; const nextState = reducer(undefined, action); expect(nextState).to.equal(fromJS({ entries: ['Trainspotting'] })); }); });
      
      







core.js



, :



 src/core.js export const INITIAL_STATE = Map();
      
      





reducer- :



 src/reducer.js import {setEntries, next, vote, INITIAL_STATE} from './core'; export default function reducer(state = INITIAL_STATE, action) { switch (action.type) { case 'SET_ENTRIES': return setEntries(state, action.entries); case 'NEXT': return next(state); case 'VOTE': return vote(state, action.entry) } return state; }
      
      







, reducer . , , . : callback-a.



 test/reducer_spec.js it('   reduce', () => { const actions = [ {type: 'SET_ENTRIES', entries: ['Trainspotting', '28 Days Later']}, {type: 'NEXT'}, {type: 'VOTE', entry: 'Trainspotting'}, {type: 'VOTE', entry: '28 Days Later'}, {type: 'VOTE', entry: 'Trainspotting'}, {type: 'NEXT'} ]; const finalState = actions.reduce(reducer, Map()); expect(finalState).to.equal(fromJS({ winner: 'Trainspotting' })); });
      
      





/ action/reducer, . actions — , JSON, , , Web Worker, reducer-a. , .



, actions , Immutable. Redux.



4.6。 Reducer-



, .



, . , . .



( ). : - , .



, . - : vote



, vote



. . unit vote



:



 test/core_spec.js describe('vote', () => { it('     ', () => { const state = Map({ pair: List.of('Trainspotting', '28 Days Later') }); const nextState = vote(state, 'Trainspotting') expect(nextState).to.equal(Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 1 }) })); }); it('       ', () => { const state = Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 3, '28 Days Later': 2 }) }); const nextState = vote(state, 'Trainspotting'); expect(nextState).to.equal(Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({ 'Trainspotting': 4, '28 Days Later': 2 }) })); }); });
      
      





, , !



vote



:



 src/core.js export function vote(voteState, entry) { return voteState.updateIn( ['tally', entry], 0, tally => tally + 1 ); }
      
      





reducer vote



.



 src/reducer.js export default function reducer(state = INITIAL_STATE, action) { switch (action.type) { case 'SET_ENTRIES': return setEntries(state, action.entries); case 'NEXT': return next(state); case 'VOTE': return state.update('vote', voteState => vote(voteState, action.entry)); } return state; }
      
      





, : -reducer reducer- . .



reducer- Redux . , reducer-.



4.7。 Redux Store



, reducer, , Redux.



, , , reduce



. , . , : , , .



Redux Store . , , .



reducer-, :



 import {createStore} from 'redux'; const store = createStore(reducer);
      
      





(dispatch) store, reducer- . , Redux-Store.



 store.dispatch({type: 'NEXT'});
      
      





:



 store.getState();
      
      





Redux Store store.js



. : , , action :



 test/store_spec.js import {Map, fromJS} from 'immutable'; import {expect} from 'chai'; import makeStore from '../src/store'; describe('store', () => { it('     ', () => { const store = makeStore(); expect(store.getState()).to.equal(Map()); store.dispatch({ type: 'SET_ENTRIES', entries: ['Trainspotting', '28 Days Later'] }); expect(store.getState()).to.equal(fromJS({ entries: ['Trainspotting', '28 Days Later'] })); }); });
      
      





Store Redux :



 npm install --save redux
      
      





store.js



, createStore



reducer-:



 src/store.js import {createStore} from 'redux'; import reducer from './reducer'; export default function makeStore() { return createStore(reducer); }
      
      





, Redux Store , — , actions, , reducer.



: Redux- ?

: . .



. , . - ?



. — , . — .



, Redux. , reducer-, Redux . , , !



index.js



, Store:



 index.js import makeStore from './src/store'; export const store = makeStore();
      
      





, Node REPL (, babel-node



), index.js



Store.



4.8. Socket.io



, . , .



, . WebSocket'. , Socket.io , WebSocket'. , WebSocket'.



Socket.io :



 npm install --save socket.io
      
      





server.js



, Socket.io:



 src/server.js import Server from 'socket.io'; export default function startServer() { const io = new Server().attach(8090); }
      
      





Socket.io, 8090 HTTP-. , , .



index.js



, :



 index.js import makeStore from './src/store'; import startServer from './src/server'; export const store = makeStore(); startServer();
      
      





, start



package.json



:



 package.json "scripts": { "start": "babel-node index.js", "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive", "test:watch": "npm run test -- --watch" },
      
      





Redux-Store:



 npm run start
      
      





babel-node



babel-cli . Node- Babel-. , , . .



4.9. Store Redux Listener



Socket.io Redux , . .



(, « ?», « ?», « ?»). Socket.io .



, - ? Redux store, , action, . , callback store.



startServer



, Redux store :



 index.js import makeStore from './src/store'; import {startServer} from './src/server'; export const store = makeStore(); startServer(store);
      
      





(listener) . , JavaScript- Socket.io state



. JSON- , Socket.io.



 src/server.js import Server from 'socket.io'; export function startServer(store) { const io = new Server().attach(8090); store.subscribe( () => io.emit('state', store.getState().toJS()) ); }
      
      





. . (, , , ..). .



, . .



Socket.io connection



, . :



 src/server.js import Server from 'socket.io'; export function startServer(store) { const io = new Server().attach(8090); store.subscribe( () => io.emit('state', store.getState().toJS()) ); io.on('connection', (socket) => { socket.emit('state', store.getState().toJS()); }); }
      
      





4.10. Remote Redux Actions



, : , NEXT



. Redux store action



, .



 src/server.js import Server from 'socket.io'; export function startServer(store) { const io = new Server().attach(8090); store.subscribe( () => io.emit('state', store.getState().toJS()) ); io.on('connection', (socket) => { socket.emit('state', store.getState().toJS()); socket.on('action', store.dispatch.bind(store)); }); }
      
      





« Redux», store (remote) actions. Redux : JavaScript-, , , . !



, , , Socket.io, Redux store. - , Vert.x Event Bus Bridge . .



:



  1. - (action).
  2. Redux store.
  3. Store reducer, , action.
  4. Store reducer- .
  5. Store listener, .
  6. state



    .
  7. — , — .


, , , , . entries.json



. .



 entries.json [ "Shallow Grave", "Trainspotting", "A Life Less Ordinary", "The Beach", "28 Days Later", "Millions", "Sunshine", "Slumdog Millionaire", "127 Hours", "Trance", "Steve Jobs" ]
      
      





index.js



, NEXT



:



 index.js import makeStore from './src/store'; import {startServer} from './src/server'; export const store = makeStore(); startServer(store); store.dispatch({ type: 'SET_ENTRIES', entries: require('./entries.json') }); store.dispatch({type: 'NEXT'});
      
      





.



5.



React-, . Redux. , : React-. , , React . , GitHub .



5.1。



NPM-, .



 mkdir voting-client cd voting-client npm init –y
      
      





HTML-. dist/index.html



:



 dist/index.html <!DOCTYPE html> <html> <body> <div id="app"></div> <script src="bundle.js"></script> </body> </html>
      
      





<div>



ID app



, . bundle.js



.



JavaScript-, . :



 src/index.js console.log('I am alive!');
      
      





Webpack , :



 npm install --save-dev webpack webpack-dev-server
      
      





, , : npm install -g webpack webpack-dev-server



.



Webpack, , :



 webpack.config.js module.exports = { entry: [ './src/index.js' ], output: { path: __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, devServer: { contentBase: './dist' } };
      
      





index.js



dist/bundle.js



. dist



.



webpack bundle.js



:



 webpack
      
      





, localhost:8080 ( index.js



).



 webpack-dev-server
      
      





React JSX ES6, . Babel , Webpack-:



 npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react
      
      





package.json



Babel' ES6/ES2015 React JSX, :



 package.json "babel": { "presets": ["es2015", "react"] }
      
      





Webpack, .jsx



.js



Babel:



 webpack.config.js module.exports = { entry: [ './src/index.js' ], module: { loaders: [{ test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel' }] }, resolve: { extensions: ['', '.js', '.jsx'] }, output: { path: __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, devServer: { contentBase: './dist' } };
      
      





CSS. , . . CSS- Webpack- ( ), , .



5.1.1.



. — Mocha Chai:



 npm install --save-dev mocha chai
      
      





React-, DOM. - Karma . , jsdom , DOM JavaScript Node:



 npm install --save-dev jsdom
      
      





jsdom io.js Node.js 4.0.0. Node, jsdom:



 npm install --save-dev jsdom@3
      
      





jsdom React. , jsdom- document



window



, . , React , document



window



. :



 test/test_helper.js import jsdom from 'jsdom'; const doc = jsdom.jsdom('<!doctype html><html><body></body></html>'); const win = doc.defaultView; global.document = doc; global.window = win;
      
      





, , jsdom- window



(, navigator



), global Node.js. , window



window.



, . React:



 test/test_helper.js import jsdom from 'jsdom'; const doc = jsdom.jsdom('<!doctype html><html><body></body></html>'); const win = doc.defaultView; global.document = doc; global.window = win; Object.keys(window).forEach((key) => { if (!(key in global)) { global[key] = window[key]; } });
      
      





Immutable , , , Chai. — immutable chai-immutable:



 npm install --save immutable npm install --save-dev chai-immutable
      
      





:



 test/test_helper.js import jsdom from 'jsdom'; import chai from 'chai'; import chaiImmutable from 'chai-immutable'; const doc = jsdom.jsdom('<!doctype html><html><body></body></html>'); const win = doc.defaultView; global.document = doc; global.window = win; Object.keys(window).forEach((key) => { if (!(key in global)) { global[key] = window[key]; } }); chai.use(chaiImmutable);
      
      





: package.json



:



 package.json "scripts": { "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js \"test/**/*@(.js|.jsx)\"" },
      
      





package.json



. : --recursive



, .jsx



-. .js



, .jsx



- glob .



. test:watch



, :



 package.json "scripts": { "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js 'test/**/*.@(js|jsx)'", "test:watch": "npm run test -- --watch" },
      
      





5.2。 React react-hot-loader



Webpack Babel , React!



React- Redux Immutable (Pure Components, Dumb Components). , , :



  1. , . — , ..
  2. . . - , , . , .


, : , , . . , .



, ? Redux store! — . React- .



. React :



 npm install --save react react-dom
      
      





react-hot-loader . .



 npm install --save-dev react-hot-loader
      
      





react-hot-loader, . , Redux react-hot-loader — !



webpack.config.js



. 起こったことは次のとおりです。



 webpack.config.js var webpack = require('webpack'); module.exports = { entry: [ 'webpack-dev-server/client?http://localhost:8080', 'webpack/hot/only-dev-server', './src/index.js' ], module: { loaders: [{ test: /\.jsx?$/, exclude: /node_modules/, loader: 'react-hot!babel' }] }, resolve: { extensions: ['', '.js', '.jsx'] }, output: { path: __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, devServer: { contentBase: './dist', hot: true }, plugins: [ new webpack.HotModuleReplacementPlugin() ] };
      
      





entry



: Webpack (hot module loader) Webpack. Webpack . , plugins



devServer



.



loaders



react-hot



, Babel .js .jsx.



(Hot Module Replacement).



5.3。



: , , . .







, React- : , . , Webpack react-hot-loader , . , , .



, Voting



. div #app



, index.html



. index.js



index.jsx



, JSX-:



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import Voting from './components/Voting'; const pair = ['Trainspotting', '28 Days Later']; ReactDOM.render( <Voting pair={pair} />, document.getElementById('app') );
      
      





Voting



. , . , , .



webpack.config.js



:



 webpack.config.js entry: [ 'webpack-dev-server/client?http://localhost:8080', 'webpack/hot/only-dev-server', './src/index.jsx' ],
      
      





webpack-dev-server Voting



. :



 src/components/Voting.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, render: function() { return <div className="voting"> {this.getPair().map(entry => <button key={entry}> <h1>{entry}</h1> </button> )} </div>; } });
      
      





, . - , . . .



, , webpack-dev-server, .



. Voting_spec.jsx



:



 test/components/Voting_spec.jsx import Voting from '../../src/components/Voting'; describe('Voting', () => { });
      
      





pair



, . renderIntoDocument React, :



 npm install --save react-addons-test-utils
      
      







 test/components/Voting_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument } from 'react-addons-test-utils'; import Voting from '../../src/components/Voting'; describe('Voting', () => { it('renders a pair of buttons', () => { const component = renderIntoDocument( <Voting pair={["Trainspotting", "28 Days Later"]} /> ); }); });
      
      





React — scryRenderedDOMComponentsWithTag . , .



 test/components/Voting_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithTag } from 'react-addons-test-utils'; import Voting from '../../src/components/Voting'; import {expect} from 'chai'; describe('Voting', () => { it('renders a pair of buttons', () => { const component = renderIntoDocument( <Voting pair={["Trainspotting", "28 Days Later"]} /> ); const buttons = scryRenderedDOMComponentsWithTag(component, 'button'); expect(buttons.length).to.equal(2); expect(buttons[0].textContent).to.equal('Trainspotting'); expect(buttons[1].textContent).to.equal('28 Days Later'); }); });
      
      





:



 npm run test
      
      





callback-. , . . Simulate React:



 test/components/Voting_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithTag, Simulate } from 'react-addons-test-utils'; import Voting from '../../src/components/Voting'; import {expect} from 'chai'; describe('Voting', () => { // ... it('invokes callback when a button is clicked', () => { let votedWith; const vote = (entry) => votedWith = entry; const component = renderIntoDocument( <Voting pair={["Trainspotting", "28 Days Later"]} vote={vote}/> ); const buttons = scryRenderedDOMComponentsWithTag(component, 'button'); Simulate.click(buttons[0]); expect(votedWith).to.equal('Trainspotting'); }); });
      
      





. onClick



, vote



:



 src/components/Voting.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, render: function() { return <div className="voting"> {this.getPair().map(entry => <button key={entry} onClick={() => this.props.vote(entry)}> <h1>{entry}</h1> </button> )} </div>; } });
      
      





: actions, callback-.



. , , .



- , . , , . hasVoted



, :



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import Voting from './components/Voting'; const pair = ['Trainspotting', '28 Days Later']; ReactDOM.render( <Voting pair={pair} hasVoted="Trainspotting" />, document.getElementById('app') );
      
      





:



 src/components/Voting.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, isDisabled: function() { return !!this.props.hasVoted; }, render: function() { return <div className="voting"> {this.getPair().map(entry => <button key={entry} disabled={this.isDisabled()} onClick={() => this.props.vote(entry)}> <h1>{entry}</h1> </button> )} </div>; } });
      
      





label , hasVoted



. hasVotedFor



, , :



 src/components/Voting.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, isDisabled: function() { return !!this.props.hasVoted; }, hasVotedFor: function(entry) { return this.props.hasVoted === entry; }, render: function() { return <div className="voting"> {this.getPair().map(entry => <button key={entry} disabled={this.isDisabled()} onClick={() => this.props.vote(entry)}> <h1>{entry}</h1> {this.hasVotedFor(entry) ? <div className="label">Voted</div> : null} </button> )} </div>; } });
      
      





, . , :



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import Voting from './components/Voting'; const pair = ['Trainspotting', '28 Days Later']; ReactDOM.render( <Voting pair={pair} winner="Trainspotting" />, document.getElementById('app') );
      
      





, div winner:



 src/components/Voting.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, isDisabled: function() { return !!this.props.hasVoted; }, hasVotedFor: function(entry) { return this.props.hasVoted === entry; }, render: function() { return <div className="voting"> {this.props.winner ? <div ref="winner">Winner is {this.props.winner}!</div> : this.getPair().map(entry => <button key={entry} disabled={this.isDisabled()} onClick={() => this.props.vote(entry)}> <h1>{entry}</h1> {this.hasVotedFor(entry) ? <div className="label">Voted</div> : null} </button> )} </div>; } });
      
      





, . , (vote screen) (winner), (vote). winner div:



 src/components/Winner.jsx import React from 'react'; export default React.createClass({ render: function() { return <div className="winner"> Winner is {this.props.winner}! </div>; } });
      
      





, , :



 src/components/Vote.jsx import React from 'react'; export default React.createClass({ getPair: function() { return this.props.pair || []; }, isDisabled: function() { return !!this.props.hasVoted; }, hasVotedFor: function(entry) { return this.props.hasVoted === entry; }, render: function() { return <div className="voting"> {this.getPair().map(entry => <button key={entry} disabled={this.isDisabled()} onClick={() => this.props.vote(entry)}> <h1>{entry}</h1> {this.hasVotedFor(entry) ? <div className="label">Voted</div> : null} </button> )} </div>; } });
      
      





, :



 src/components/Voting.jsx import React from 'react'; import Winner from './Winner'; import Vote from './Vote'; export default React.createClass({ render: function() { return <div> {this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <Vote {...this.props} />} </div>; } });
      
      





, ref . DOM-.



! , : , , callback-. . , Redux store.



. hasVoted



:



 test/components/Voting_spec.jsx it(' ,    ', () => { const component = renderIntoDocument( <Voting pair={["Trainspotting", "28 Days Later"]} hasVoted="Trainspotting" /> ); const buttons = scryRenderedDOMComponentsWithTag(component, 'button'); expect(buttons.length).to.equal(2); expect(buttons[0].hasAttribute('disabled')).to.equal(true); expect(buttons[1].hasAttribute('disabled')).to.equal(true); });
      
      





Label Voted



, hasVoted



:



 test/components/Voting_spec.jsx it(' label  ,   ', () => { const component = renderIntoDocument( <Voting pair={["Trainspotting", "28 Days Later"]} hasVoted="Trainspotting" /> ); const buttons = scryRenderedDOMComponentsWithTag(component, 'button'); expect(buttons[0].textContent).to.contain('Voted'); });
      
      





, , ref' :



 test/components/Voting_spec.jsx it('  ', () => { const component = renderIntoDocument( <Voting winner="Trainspotting" /> ); const buttons = scryRenderedDOMComponentsWithTag(component, 'button'); expect(buttons.length).to.equal(0); const winner = ReactDOM.findDOMNode(component.refs.winner); expect(winner).to.be.ok; expect(winner.textContent).to.contain('Trainspotting'); });
      
      





, , «». , , , .



5.4。 (Pure Rendering)



, , , React. , , React .



PureRenderMixin add-on- . mixin , React - ( ) . , , .



, immutable . , , !



. , , , - , :



 test/components/Voting_spec.jsx it('   ', () => { const pair = ['Trainspotting', '28 Days Later']; const container = document.createElement('div'); let component = ReactDOM.render( <Voting pair={pair} />, container ); let firstButton = scryRenderedDOMComponentsWithTag(component, 'button')[0]; expect(firstButton.textContent).to.equal('Trainspotting'); pair[0] = 'Sunshine'; component = ReactDOM.render( <Voting pair={pair} />, container ); firstButton = scryRenderedDOMComponentsWithTag(component, 'button')[0]; expect(firstButton.textContent).to.equal('Trainspotting'); });
      
      





renderIntoDocument



<div>



, .



, :



 test/components/Voting_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithTag, Simulate } from 'react-addons-test-utils'; import {List} from 'immutable'; import Voting from '../../src/components/Voting'; import {expect} from 'chai'; describe('Voting', () => { // ... it(' DOM   ', () => { const pair = List.of('Trainspotting', '28 Days Later'); const container = document.createElement('div'); let component = ReactDOM.render( <Voting pair={pair} />, container ); let firstButton = scryRenderedDOMComponentsWithTag(component, 'button')[0]; expect(firstButton.textContent).to.equal('Trainspotting'); const newPair = pair.set(0, 'Sunshine'); component = ReactDOM.render( <Voting pair={newPair} />, container ); firstButton = scryRenderedDOMComponentsWithTag(component, 'button')[0]; expect(firstButton.textContent).to.equal('Sunshine'); }); });
      
      





, PureRenderMixin. . , , : . , .



, PureRenderMixin . :



 npm install --save react-addons-pure-render-mixin
      
      





:



 src/components/Voting.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import Winner from './Winner'; import Vote from './Vote'; export default React.createClass({ mixins: [PureRenderMixin], // ... }); src/components/Vote.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], // ... }); src/components/Winner.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], // ... });
      
      





, PureRenderMixin , . , React Voting, .



PureRenderMixin . -, , -, .



5.5。 (Routing Handling)



, : .



, , . , .



, , . URL'. #/



, #/results



— .



react-router , . :



 npm install --save react-router@2.0.0
      
      





. (Router) React- Route



, . :



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Route} from 'react-router'; import App from './components/App'; import Voting from './components/Voting'; const pair = ['Trainspotting', '28 Days Later']; const routes = <Route component={App}> <Route path="/" component={Voting} /> </Route>; ReactDOM.render( <Voting pair={pair} />, document.getElementById('app') );
      
      





, Voting. , . App



, .



. App



:



 src/components/App.jsx import React from 'react'; import {List} from 'immutable'; const pair = List.of('Trainspotting', '28 Days Later'); export default React.createClass({ render: function() { return React.cloneElement(this.props.children, {pair: pair}); } });
      
      





, children



. react-router , . Voting



, Voting



.



, pair



index.jsx



App.jsx



. pair



API cloneElement . , .



, PureRenderMixin . App: - React . , .



index.js



, , :



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import App from './components/App'; import Voting from './components/Voting'; const routes = <Route component={App}> <Route path="/" component={Voting} /> </Route>; ReactDOM.render( <Router history={hashHistory}>{routes}</Router>, document.getElementById('app') );
      
      





Router



react-router



, #hash



( API HTML 5). .



: Voting



. React, . , Results



:



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import App from './components/App'; import Voting from './components/Voting'; import Results from './components/Results'; const routes = <Route component={App}> <Route path="/results" component={Results} /> <Route path="/" component={Voting} /> </Route>; ReactDOM.render( <Router history={hashHistory}>{routes}</Router>, document.getElementById('app') );
      
      





<Route>



/results



results



. Voting.



Results



:



 src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], render: function() { return <div>Hello from results!</div> } });
      
      





localhost :8080/#/results, Results. . «» «» , . , !



React. , .



, Results, - . , Voting:



 src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, render: function() { return <div className="results"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> </div> )} </div>; } });
      
      





, , . App Map:



 src/components/App.jsx import React from 'react'; import {List, Map} from 'immutable'; const pair = List.of('Trainspotting', '28 Days Later'); const tally = Map({'Trainspotting': 5, '28 Days Later': 4}); export default React.createClass({ render: function() { return React.cloneElement(this.props.children, { pair: pair, tally: tally }); } });
      
      





Results :



 src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, getVotes: function(entry) { if (this.props.tally && this.props.tally.has(entry)) { return this.props.tally.get(entry); } return 0; }, render: function() { return <div className="results"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> <div className="voteCount"> {this.getVotes(entry)} </div> </div> )} </div>; } });
      
      





Results, , . div' , . , :



 test/components/Results_spec.jsx import React from 'react'; import { renderIntoDocument, scryRenderedDOMComponentsWithClass } from 'react-addons-test-utils'; import {List, Map} from 'immutable'; import Results from '../../src/components/Results'; import {expect} from 'chai'; describe('Results', () => { it('renders entries with vote counts or zero', () => { const pair = List.of('Trainspotting', '28 Days Later'); const tally = Map({'Trainspotting': 5}); const component = renderIntoDocument( <Results pair={pair} tally={tally} /> ); const entries = scryRenderedDOMComponentsWithClass(component, 'entry'); const [train, days] = entries.map(e => e.textContent); expect(entries.length).to.equal(2); expect(train).to.contain('Trainspotting'); expect(train).to.contain('5'); expect(days).to.contain('28 Days Later'); expect(days).to.contain('0'); }); });
      
      





«Next», . , callback-. , «Next». , , :



 test/components/Results_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate } from 'react-addons-test-utils'; import {List, Map} from 'immutable'; import Results from '../../src/components/Results'; import {expect} from 'chai'; describe('Results', () => { // ... it(' callback    Next', () => { let nextInvoked = false; const next = () => nextInvoked = true; const pair = List.of('Trainspotting', '28 Days Later'); const component = renderIntoDocument( <Results pair={pair} tally={Map()} next={next}/> ); Simulate.click(ReactDOM.findDOMNode(component.refs.next)); expect(nextInvoked).to.equal(true); }); });
      
      





. , :



 src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; export default React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, getVotes: function(entry) { if (this.props.tally && this.props.tally.has(entry)) { return this.props.tally.get(entry); } return 0; }, render: function() { return <div className="results"> <div className="tally"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> <div class="voteCount"> {this.getVotes(entry)} </div> </div> )} </div> <div className="management"> <button ref="next" className="next" onClick={this.props.next}> Next </button> </div> </div>; } });
      
      





, :



 test/components/Results_spec.jsx it('  ', () => { const component = renderIntoDocument( <Results winner="Trainspotting" pair={["Trainspotting", "28 Days Later"]} tally={Map()} /> ); const winner = ReactDOM.findDOMNode(component.refs.winner); expect(winner).to.be.ok; expect(winner.textContent).to.contain('Trainspotting'); });
      
      





Winner, . , :



 src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import Winner from './Winner'; export default React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, getVotes: function(entry) { if (this.props.tally && this.props.tally.has(entry)) { return this.props.tally.get(entry); } return 0; }, render: function() { return this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <div className="results"> <div className="tally"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> <div className="voteCount"> {this.getVotes(entry)} </div> </div> )} </div> <div className="management"> <button ref="next" className="next" onClick={this.props.next}> Next </button> </div> </div>; } });
      
      





. , Tally . , !



, . , . , . , .



, , , Redux store .



5.6。 Redux Store



Redux , . . Redux , ! , React-.



, . , .



. , . vote :







, .







(Voting) , . :







, :







, , hasVoted



. , (actions) (reducers), Redux store. ?



, . — . :





, . . .



, . , state



, . , . reducer-a, action, . action :



 { type: 'SET_STATE', state: { vote: {...} } }
      
      





, . , , reducer :



 test/reducer_spec.js import {List, Map, fromJS} from 'immutable'; import {expect} from 'chai'; import reducer from '../src/reducer'; describe('reducer', () => { it('handles SET_STATE', () => { const initialState = Map(); const action = { type: 'SET_STATE', state: Map({ vote: Map({ pair: List.of('Trainspotting', '28 Days Later'), tally: Map({Trainspotting: 1}) }) }) }; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } })); }); });
      
      





Reducer JS- . :



 test/reducer_spec.js it(' SET_STATE   JS-', () => { const initialState = Map(); const action = { type: 'SET_STATE', state: { vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } } }; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } })); });
      
      





undefined



reducer- :



 test/reducer_spec.js it(' SET_STATE   ', () => { const action = { type: 'SET_STATE', state: { vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } } }; const nextState = reducer(undefined, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } })); });
      
      





. , . -reducer, reducer-:



 src/reducer.js import {Map} from 'immutable'; export default function(state = Map(), action) { return state; }
      
      





Reducer action SET_STATE



. merge Map -. !



 src/reducer.js import {Map} from 'immutable'; function setState(state, newState) { return state.merge(newState); } export default function(state = Map(), action) { switch (action.type) { case 'SET_STATE': return setState(state, action.state); } return state; }
      
      





, «» , reducer-. , , . , . , .



, : «Next». , , .



Redux :



 npm install --save redux
      
      





store index.jsx



. - , SET_STATE



( , ):



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import reducer from './reducer'; import App from './components/App'; import Voting from './components/Voting'; import Results from './components/Results'; const store = createStore(reducer); store.dispatch({ type: 'SET_STATE', state: { vote: { pair: ['Sunshine', '28 Days Later'], tally: {Sunshine: 2} } } }); const routes = <Route component={App}> <Route path="/results" component={Results} /> <Route path="/" component={Voting} /> </Route>; ReactDOM.render( <Router history={hashHistory}>{routes}</Router>, document.getElementById('app') );
      
      





Store . React-?



5.7。 Redux React



Redux Store . React-, . store , . React , PureRenderMixin , , .



, Redux React react-redux :



 npm install --save react-redux
      
      





react-redux Redux store :





- ( Provider ) react-redux. Redux Store, store c .



-. .



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import reducer from './reducer'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import Results from './components/Results'; const store = createStore(reducer); store.dispatch({ type: 'SET_STATE', state: { vote: { pair: ['Sunshine', '28 Days Later'], tally: {Sunshine: 2} } } }); const routes = <Route component={App}> <Route path="/results" component={Results} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') ); });
      
      





, «», store . , :





Voting



. react-redux connect , . , , React-:



 connect(mapStateToProps)(SomeComponent);
      
      





- Redux Store . . Voting pair



winner



Store:



 src/components/Voting.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin' import {connect} from 'react-redux'; import Winner from './Winner'; import Vote from './Vote'; const Voting = React.createClass({ mixins: [PureRenderMixin], render: function() { return <div> {this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <Vote {...this.props} />} </div>; } }); function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), winner: state.get('winner') }; } connect(mapStateToProps)(Voting); export default Voting;
      
      





. , connect



Voting



. , . connect



Voting



. , . VotingContainer



:



 src/components/Voting.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import {connect} from 'react-redux'; import Winner from './Winner'; import Vote from './Vote'; export const Voting = React.createClass({ mixins: [PureRenderMixin], render: function() { return <div> {this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <Vote {...this.props} />} </div>; } }); function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), winner: state.get('winner') }; } export const VotingContainer = connect(mapStateToProps)(Voting);
      
      





Voting



VotingContainer



. react-redux «» (dumb) , — «» (smart). «» «». , , , :





-, Voting



VotingContainer



. , Redux-.



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import reducer from './reducer'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import Results from './components/Results'; const store = createStore(reducer); store.dispatch({ type: 'SET_STATE', state: { vote: { pair: ['Sunshine', '28 Days Later'], tally: {Sunshine: 2} } } }); const routes = <Route component={App}> <Route path="/results" component={Results} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
      
      





Voting



, Voting :



 test/components/Voting_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithTag, Simulate } from 'react-addons-test-utils'; import {List} from 'immutable'; import {Voting} from '../../src/components/Voting'; import {expect} from 'chai';
      
      





. Voting, . , store.



, pair



winner



. , tally



:



 src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import {connect} from 'react-redux'; import Winner from './Winner'; export const Results = React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, getVotes: function(entry) { if (this.props.tally && this.props.tally.has(entry)) { return this.props.tally.get(entry); } return 0; }, render: function() { return this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <div className="results"> <div className="tally"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> <div className="voteCount"> {this.getVotes(entry)} </div> </div> )} </div> <div className="management"> <button ref="next" className="next" onClick={this.props.next}> Next </button> </div> </div>; } }); function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), tally: state.getIn(['vote', 'tally']), winner: state.get('winner') } } export const ResultsContainer = connect(mapStateToProps)(Results);
      
      





index.jsx



, Results



ResultsContainer



:



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import reducer from './reducer'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import {ResultsContainer} from './components/Results'; const store = createStore(reducer); store.dispatch({ type: 'SET_STATE', state: { vote: { pair: ['Sunshine', '28 Days Later'], tally: {Sunshine: 2} } } }); const routes = <Route component={App}> <Route path="/results" component={ResultsContainer} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
      
      





, Results



:



 test/components/Results_spec.jsx import React from 'react'; import ReactDOM from 'react-dom'; import { renderIntoDocument, scryRenderedDOMComponentsWithClass, Simulate } from 'react-addons-test-utils'; import {List, Map} from 'immutable'; import {Results} from '../../src/components/Results'; import {expect} from 'chai';
      
      





React- Redux-, .



, , . .



, , . , . , , , . «».



, Redux. App.jsx



, :



 src/components/App.jsx import React from 'react'; export default React.createClass({ render: function() { return this.props.children; } });
      
      





5.8. Socket.io



Redux-, Redux-. , .



socket- . Redux-, . .



. Socket.io- . socket.io-client , , :



 npm install --save socket.io-client
      
      





io



, Socket.io. 8090 ( ):



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import io from 'socket.io-client'; import reducer from './reducer'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import {ResultsContainer} from './components/Results'; const store = createStore(reducer); store.dispatch({ type: 'SET_STATE', state: { vote: { pair: ['Sunshine', '28 Days Later'], tally: {Sunshine: 2} } } }); const socket = io(`${location.protocol}//${location.hostname}:8090`); const routes = <Route component={App}> <Route path="/results" component={ResultsContainer} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
      
      





, , . WebSocket-, Socket.io.



Socket.io-: , Webpack-.



5.9. actions



Socket.io . state



, . SET_STATE



. reducer:



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import io from 'socket.io-client'; import reducer from './reducer'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import {ResultsContainer} from './components/Results'; const store = createStore(reducer); const socket = io(`${location.protocol}//${location.hostname}:8090`); socket.on('state', state => store.dispatch({type: 'SET_STATE', state}) ); const routes = <Route component={App}> <Route path="/results" component={ResultsContainer} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
      
      





, SET_STATE



. , .



— : , . !



5.10. actions React-



, Redux store. .



. , Voting



vote



, callback-. , . , .



, - ? , . , : hasVoted



, - .



SET_STATE



Redux action — VOTE



. hasVoted



:



 test/reducer_spec.js it(' VOTE    hasVoted', () => { const state = fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } }); const action = {type: 'VOTE', entry: 'Trainspotting'}; const nextState = reducer(state, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} }, hasVoted: 'Trainspotting' })); });
      
      





, VOTE - , :



 test/reducer_spec.js it('      hasVoted  VOTE', () => { const state = fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } }); const action = {type: 'VOTE', entry: 'Sunshine'}; const nextState = reducer(state, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} } })); });
      
      





reducer-a :



 src/reducer.js import {Map} from 'immutable'; function setState(state, newState) { return state.merge(newState); } function vote(state, entry) { const currentPair = state.getIn(['vote', 'pair']); if (currentPair && currentPair.includes(entry)) { return state.set('hasVoted', entry); } else { return state; } } export default function(state = Map(), action) { switch (action.type) { case 'SET_STATE': return setState(state, action.state); case 'VOTE': return vote(state, action.entry); } return state; }
      
      





hasVoted



. , . SET_STATE



, , , . , hasVoted



:



 test/reducer_spec.js it('  ,   hasVoted  SET_STATE', () => { const initialState = fromJS({ vote: { pair: ['Trainspotting', '28 Days Later'], tally: {Trainspotting: 1} }, hasVoted: 'Trainspotting' }); const action = { type: 'SET_STATE', state: { vote: { pair: ['Sunshine', 'Slumdog Millionaire'] } } }; const nextState = reducer(initialState, action); expect(nextState).to.equal(fromJS({ vote: { pair: ['Sunshine', 'Slumdog Millionaire'] } })); });
      
      





resetVote



SET_STATE



:



 src/reducer.js import {List, Map} from 'immutable'; function setState(state, newState) { return state.merge(newState); } function vote(state, entry) { const currentPair = state.getIn(['vote', 'pair']); if (currentPair && currentPair.includes(entry)) { return state.set('hasVoted', entry); } else { return state; } } function resetVote(state) { const hasVoted = state.get('hasVoted'); const currentPair = state.getIn(['vote', 'pair'], List()); if (hasVoted && !currentPair.includes(hasVoted)) { return state.remove('hasVoted'); } else { return state; } } export default function(state = Map(), action) { switch (action.type) { case 'SET_STATE': return resetVote(setState(state, action.state)); case 'VOTE': return vote(state, action.entry); } return state; }
      
      





hasVoted



. .



hasVoted



Voting



:



 src/components/Voting.jsx function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), hasVoted: state.get('hasVoted'), winner: state.get('winner') }; }
      
      





- Voting vote



callback, . Voting



actions Redux, connect



react-redux.



react-redux , . Redux: (Action creators) .



, Redux , ( ) type



. . :



 function vote(entry) { return {type: 'VOTE', entry}; }
      
      





« ». , . , . , . , .



, :



 src/action_creators.js export function setState(state) { return { type: 'SET_STATE', state }; } export function vote(entry) { return { type: 'VOTE', entry }; }
      
      





. , . , .



index.jsx



Socket.io- setState



:



 src/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import io from 'socket.io-client'; import reducer from './reducer'; import {setState} from './action_creators'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import {ResultsContainer} from './components/Results'; const store = createStore(reducer); const socket = io(`${location.protocol}//${location.hostname}:8090`); socket.on('state', state => store.dispatch(setState(state)) ); const routes = <Route component={App}> <Route path="/results" component={ResultsContainer} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
      
      





, react-redux React-. callback- vote



Voting



vote. , : , , . connect



react-redux :



 src/components/Voting.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import {connect} from 'react-redux'; import Winner from './Winner'; import Vote from './Vote'; import * as actionCreators from '../action_creators'; export const Voting = React.createClass({ mixins: [PureRenderMixin], render: function() { return <div> {this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <Vote {...this.props} />} </div>; } }); function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), hasVoted: state.get('hasVoted'), winner: state.get('winner') }; } export const VotingContainer = connect( mapStateToProps, actionCreators )(Voting);
      
      





vote



Voting



. , vote, Redux Store. ! : .



5.11. Redux Middleware



— . “Next” .



. ?





, , . VOTE



, Redux stores. .



どこから始めますか? Redux , . , .



Redux actions, redux store — Middleware .



Middleware () — , , reducer store. Middleware , , store. actions .



middleware listeners:





.



remote action middleware, Socket.io- store, .



middleware. , Redux store , callback «next». , Redux action. middleware:



 src/remote_action_middleware.js export default store => next => action => { }
      
      





, :



 export default function(store) { return function(next) { return function(action) { } } }
      
      





. : ( function(store, next, action) { }



), . , «», store



.



next



. callback, middleware , action



store ( middleware):



 src/remote_action_middleware.js export default store => next => action => { return next(action); }
      
      





next



, . reducer store.



- middleware, , :



 src/remote_action_middleware.js export default store => next => action => { console.log('in middleware', action); return next(action); }
      
      





middleware Redux store, . middleware Redux applyMiddleware



. middleware, , , , , createStore



. store middleware:



 src/components/index.jsx import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, hashHistory} from 'react-router'; import {createStore, applyMiddleware} from 'redux'; import {Provider} from 'react-redux'; import io from 'socket.io-client'; import reducer from './reducer'; import {setState} from './action_creators'; import remoteActionMiddleware from './remote_action_middleware'; import App from './components/App'; import {VotingContainer} from './components/Voting'; import {ResultsContainer} from './components/Results'; const createStoreWithMiddleware = applyMiddleware( remoteActionMiddleware )(createStore); const store = createStoreWithMiddleware(reducer); const socket = io(`${location.protocol}//${location.hostname}:8090`); socket.on('state', state => store.dispatch(setState(state)) ); const routes = <Route component={App}> <Route path="/results" component={ResultsContainer} /> <Route path="/" component={VotingContainer} /> </Route>; ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
      
      





. API Redux.



, , , middleware actions: SET_STATE



, — VOTE



.



Middleware Socket.io- middleware. . index.jsx



, middleware . middleware. Socket.io:



 src/remote_action_middleware.js export default socket => store => next => action => { console.log('in middleware', action); return next(action); }
      
      





 src/index.jsx const socket = io(`${location.protocol}//${location.hostname}:8090`); socket.on('state', state => store.dispatch(setState(state)) ); const createStoreWithMiddleware = applyMiddleware( remoteActionMiddleware(socket) )(createStore); const store = createStoreWithMiddleware(reducer);
      
      







, store: , store.



, middleware action



:



 src/remote_action_middleware.js export default socket => store => next => action => { socket.emit('action', action); return next(action); }
      
      





以上です! . , . !



: SET_STATE



, . , , SET_STATE



. .



Middleware action . , SET_STATE, , . , {meta: {remote: true}}



:



( rafScheduler middleware )



 src/remote_action_middleware.js export default socket => store => next => action => { if (action.meta && action.meta.remote) { socket.emit('action', action); } return next(action); }
      
      





VOTE



, SET_STATE



:



 src/action_creators.js export function setState(state) { return { type: 'SET_STATE', state }; } export function vote(entry) { return { meta: {remote: true}, type: 'VOTE', entry }; }
      
      





:



  1. . VOTE.
  2. Middleware action Socket.io-.
  3. Redux store, hasVote



    .
  4. , Redux store action .
  5. store .
  6. Redux store SET_STATE



    .
  7. .


“Next”. , . .



NEXT



:



 src/action_creator.js export function setState(state) { return { type: 'SET_STATE', state }; } export function vote(entry) { return { meta: {remote: true}, type: 'VOTE', entry }; } export function next() { return { meta: {remote: true}, type: 'NEXT' }; }
      
      





ResultsContainer



:



 src/components/Results.jsx import React from 'react'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import {connect} from 'react-redux'; import Winner from './Winner'; import * as actionCreators from '../action_creators'; export const Results = React.createClass({ mixins: [PureRenderMixin], getPair: function() { return this.props.pair || []; }, getVotes: function(entry) { if (this.props.tally && this.props.tally.has(entry)) { return this.props.tally.get(entry); } return 0; }, render: function() { return this.props.winner ? <Winner ref="winner" winner={this.props.winner} /> : <div className="results"> <div className="tally"> {this.getPair().map(entry => <div key={entry} className="entry"> <h1>{entry}</h1> <div className="voteCount"> {this.getVotes(entry)} </div> </div> )} </div> <div className="management"> <button ref="next" className="next" onClick={this.props.next}> Next </button> </div> </div>; } }); function mapStateToProps(state) { return { pair: state.getIn(['vote', 'pair']), tally: state.getIn(['vote', 'tally']), winner: state.get('winner') } } export const ResultsContainer = connect( mapStateToProps, actionCreators )(Results);
      
      





… ! . . , . . «Next» , .



6.



, Redux, . .



1.

, . , .



.



2.

, , hasVoted. : , , . , .



, , .



: . , . , .



.



3.

. , . .



: , , . , . , .



.



4.

, .



: , .



.



5.

Socket.io . , .



: Socket.io , Redux- .



.



: (Peer to Peer)

, , . , reducer- reducer , . , .



, , ? ? P2P WebRTC? ( Socket.io P2P )



All Articles