📜 ⬆️ ⬇️

Redux Tutorial

Today, Redux is one of the most interesting phenomena in the world of JavaScript. It stands out from hundreds of libraries and frameworks in that it competently solves many different issues by introducing a simple and predictable state model, a bias for functional programming and immutable data, providing a compact API. What else is needed for happiness? Redux is a very small library, and learning its API is not difficult. But for many people, there is a kind of pattern rupture - a small number of components and voluntary restrictions on pure functions and immutable data may seem unwarranted coercion. How exactly do we work in such conditions?

In this tutorial, we will look at building a full-stack application from scratch using Redux and Immutable-js . Applying the TDD approach, we will go through all the stages of constructing a Node + Redux backend and React + Redux frontend applications. In addition, we will use tools such as ES6, Babel , Socket.io , Webpack and Mocha . The set is very curious, and you instantly master it!

The content of the article


1. What you need
2. Application
3. Architecture
4. Server application
4.1. Development of an application state tree
4.2. Project Setup
4.3. Introduction to immutable data
4.4. Implementing application logic with pure functions
4.4.1. Loading records
4.4.2. Launch vote
4.4.3. Voting
4.4.4. Go to the next pair
4.4.5. Completion of the vote
4.5. Using Actions and Reducers
4.6. Flavor Reducer Composition
4.7. Using the Redux Store
4.8. Configuring Socket.io Server
4.9. Status Translation from Redux Listener
4.10. Getting Redux Remote Actions
5. Client application
5.1. Customize client project
5.1.1. Unit testing support
5.2. React and react-hot-loader
5.3. Creating an interface for the voting screen
5.4. Immutable data and pure rendering
5.5. Creating an interface for the results screen and processing routing
5.6. Using Client Redux-Store
5.7. Transferring input from Redux to React
5.8. Configuring the Socket.io client
5.9. Getting actions from the server
5.10. Transfer actions from React components
5.11. Sending actions to the server using Redux Middleware
6. Exercises

1. What you need


This guide will be most useful for developers who already know how to write JavaScript applications. As already mentioned, we will use Node, ES6, React , Webpack and Babel , and if you are at least a little familiar with these tools, there will be no problems with promotion. Even if you are not familiar, you can understand the basics along the way.
')
As a good tutorial on developing web applications using React, Webpack and ES6, you can advise SurviveJS . As for the tools, you will need a Node with NPM and your favorite text editor.

2. Application


We will make an application for "live" voting at parties, conferences, meetings and other meetings. The idea is that the user will be offered a collection of voting positions: movies, songs, programming languages, quotes from Horse JS , and so on. The app will have items in pairs so that everyone can vote for their favorite. As a result of a series of polls, one element will remain - the winner. Example of voting for the best movie Danny Boyle :



The application will have two different user interfaces:




3. Architecture


Structurally, the system will consist of two applications:


Interaction between applications will be carried out using WebSockets. Redux will help us to organize the code of the client and server parts. And for storing states we will use the Immutable structure.

Despite the great similarity between the client and the server — for example, both will use Redux — this is not a universal / isomorphic application , and applications will not share any code. Rather, it can be described as a distributed system of two applications that interact with each other through message passing.

4. Server application


First we write a Node application, and then React. This will allow us not to be distracted from the implementation of the basic logic of the application, before we get to the interface. Since we are creating a server application, we will get acquainted with Redux and Immutable and find out how the application built on them will be arranged. Redux is usually associated with React projects, but its use is not limited to them. In particular, we will find out how Redux can be useful in other contexts!

In the course of reading this guide, I recommend that you write an application from scratch, but you can download the source code from GitHub .

4.1. Development of an application state tree


Creating an application using Redux often begins with a thinking through the application state data structure. It describes what happens in the application at any time. State (state) is in any framework and architecture. In Ember and Backbone applications, the state is stored in Models. In Angular-based applications, the state is most often stored in Factories and Services. In most Flux applications, the state is a repository (Stores). And how is this done in Redux?

Its main difference is that all application states are stored in a single tree structure. Thus, everything that needs to be known about the state of the application is contained in one data structure from associative (map) and regular arrays. As you will soon see, this decision has quite a few consequences. One of the most important is that you can separate the state of the application from its behavior . State is pure data. It does not contain any methods or functions, and it is not hidden inside other objects. Everything is in one place. This may seem to be a limitation, especially if you have experience with object-oriented programming. But in reality this is a manifestation of greater freedom, since you can concentrate on data alone. Very much logically flows from the design of the state of the application if you devote enough time to it.

I do not want to say that you always need to first fully develop the state tree, and then create the other components of the application. This is usually done in parallel. But it seems to me that it is more useful to first outline in general how a tree should look in different situations before starting to write code. Let's imagine what a state tree can be for our voting application. The purpose of the application is to be able to vote within pairs of objects (movies, musical groups). As an initial state of the application, it is advisable to make a simple collection of positions that will participate in the voting. Let's call this collection of entries :



After the start of voting, it is necessary to somehow separate the positions that are participating in the voting at the moment. The state may be a voice entity containing a pair of positions from which the user must select one. Naturally, this pair should be extracted from the entries collection:



We also need to keep records of voting results. This can be done using another structure inside the vote :



At the end of the current voting, the losing entry is discarded, and the winning one is returned back to the entries and placed at the end of the list. Later, she will again vote. Then the following pair is taken from the list:



These states cycle through each other as long as there are entries in the collection. At the end there will be only one record, which is declared the winner, and the voting ends:



The scheme seems quite reasonable, we will begin to implement it. There are many different ways of developing states for these requirements. Perhaps this option is not optimal. But this is not particularly important. The initial scheme should be just good to start. The main thing is that we have an understanding of how our application should work. And this is before we move on to writing code!

4.2. Project Setup


It's time to roll up your sleeves. First you need to create a project folder, and then initialize it as an NPM project:

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

In the created folder there is a single package.json file for now. We will write the code in the specification of ES6. Although Node since version 4.0.0 supports many features of ES6, the modules we need are still left behind. Therefore, we need to add Babel to our project so that we can use all the power of ES6 and translate the code into ES5:

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

We also need libraries for writing unit tests:

 npm install --save-dev mocha chai 

We'll use Mocha as a testing framework. Inside the tests, we will use Chai as a library to check the expected behavior and states. We will run the tests using the mocha :

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

After that, Mocha will recursively search all the tests of the project and run them. Babel will be used to transport the ES6 code before launching it. For convenience, you can store this command in package.json :

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

Now we need to include support for ES6 / ES2015 in Babel. To do this, we activate the babel-preset-es2015 package that we have already installed. Then just add the "babel" section to the package.json :

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

Now, using the npm command, we can run our tests:

 npm run test 

The test:watch can be used to start a process that tracks changes in our code and runs tests after each change:

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

The Immutable library developed by Facebook provides us with a number of useful data structures. We will discuss it in the next chapter, but for now just add to the project along with the chai-immutable library, which adds support for comparing Immutable-structures to Chai:

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

It is necessary to connect chai-immutable before running any tests. This can be done using the test_helper file:

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

Now we will make Mocha load this file before running the tests:

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

Now we have everything to start.

4.3. Introduction to immutable data


The second important point related to the Redux architecture: a state is not just a tree, but an immutable tree . The tree structure from the previous chapter may suggest that the code should change the state of the application simply by updating the trees: replacing elements in associative arrays, removing them from arrays, etc. But in Redux everything is done differently. The state tree in a Redux application is an immutable data structure . This means that as long as the tree exists, it does not change. It always keeps the same state. And the transition to another state is done by creating another tree, in which the necessary changes have been made. That is, two successive application states are stored in two separate and independent trees. And switching between trees is done by calling a function that accepts the current state and returns the following.



Is this a good idea? Usually they immediately indicate that if all the states are stored in one tree and you make all these safe updates, then you can save the history of the application states with little effort. This allows you to implement undo / redo “for free” - you can simply set the previous or next state (tree) from the history. You can also serialize the story and save it for the future, or put it in storage for later playback, which can be invaluable help in debugging.

But it seems to me that, in addition to all these additional features, the main advantage of using immutable data is to simplify the code. You have to program pure functions : they only accept and return data, and nothing more. These functions behave predictably. You can call them as many times as you want, and they will always behave the same way. Give them the same arguments, and you will get the same results. Testing becomes trivial, because you do not need to configure stubs or other fakes to “prepare the universe” for the function call. There are just input and output data.

Since we will describe the state of our application with immutable structures, let's spend a little time getting to know them by writing a few unit tests that illustrate the work.

If you confidently work with immutable data and the Immutable library, you can proceed to the next section.

To get acquainted with the idea of ​​immutability, you can start by talking about the simplest data structure. Suppose you have a counter application whose state is a number. Let's say it changes from 0 to 1, then to 2, then to 3, etc. In principle, we already think of numbers as immutable data. When the counter increases, the number does not change . Yes, it is impossible, because the numbers do not have "setters". You cannot say 42.setValue(43) .

So we just get another number, adding one to the previous one. This can be done using the clean function. Its argument will be the current state, and the return value will be used as the next state. The called function does not change the current state. Here is her example, as well as the unit test for her:

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

Obviously, state does not change when you call increment , because the numbers are immutable!

As you can see, this test does nothing with our application, we haven’t written it yet.

Tests can be just a learning tool for us. I often find it useful to learn new APIs or techniques by writing unit tests that run some ideas. In the book Test-Driven Development, such tests are called "training tests."

Now we will extend the idea of ​​immutability to all types of data structures, and not just numbers.

With the help of Immutable lists, we can, for example, make an application whose state is the list of films. The operation of adding a new movie will create a new list, which is a combination of the old list and the position being added . It is important to note that after this operation the old state remains unchanged :

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

And if we inserted the film into a regular array, the old state would change. But instead, we use lists from Immutable, so we use the same semantics as in the previous example with numbers.

When inserted into a regular array, the old state would change. But since we use Immutable lists, we have the same semantics as in the example with numbers.

This idea is also well applicable to full state trees. The tree is a nested structure of lists (lists), associative arrays ( maps ) and other types of collections. The operation applied to it creates a new state tree , leaving the previous one intact. If the tree is an associative array with the key movies containing the list of movies, then adding a new position implies the need to create a new array in which the key movies indicates a new list:

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


Here we see exactly the same behavior as before, extended to demonstrate the work with nested structures. The idea of ​​immutability applies to data of all shapes and sizes.

For operations on such nested structures in Immutable, there are several auxiliary functions that make it easier to “climb” into nested data in order to get the updated value. For brevity, we can use the update function:

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

We will use a similar function in our application to update the status of the application. The Immutable API hides many other features, and we only looked at the tip of the iceberg.

Immutable data is a key aspect of the Redux architecture, but there is no strict requirement to use the Immutable library. The official Redux documentation for the most part refers to simple objects and arrays of JavaScript, and refrain from modifying them by agreement .

There are several reasons why the Immutable library will be used in our manual:


4.4. Implementing application logic with pure functions


Acquainted with the idea of ​​immutable state trees and functions that operate on these trees, you can proceed to creating the logic of our application. It will be based on the above components: a tree structure and a set of functions that create new versions of this tree.

4.4.1. Loading records


First of all, the application must "download" a collection of records for voting. You can make the function setEntries , which takes the previous state and collection, and creates a new state by including records there. Here is a test for this function:

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

The initial implementation of setEntries does only the simplest thing: the entries key in the associative state array assigns the specified list of entries as the value. We receive the first of the trees designed by us earlier.

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

For convenience, let the input records be a regular JavaScript array (or something to be iterated ). In the status tree, however, there should be an Immutable list ( 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') })); }); 

To meet this requirement, we will transfer the entries to the list constructor:

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

4.4.2. Launch vote


Voting can be started by calling the next function in a state that already has a set of entries. Thus, the transition from the first to the second of the projected trees will be made.

This function does not need additional arguments. It should create an associative array of vote , in which the first two records are stored by the pair key. In this case, the entries that are currently participating in the voting should no longer be in the list of 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') })); }); }); }); 

The function implementation will merge the update with the old state, separating the first entries into a separate list, and the rest into a new version of the list of 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. Voting


As the voting proceeds, the user should be able to vote for different entries. And with each new vote on the screen should display the current result. If you have already voted for a specific entry, then its counter should increase.

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

Using the fromJS function from Immutable, you can more concisely create all these nested schemes and lists.

Run the tests:

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

Using updateIn allows you not to spread the idea of ​​the tree. This code says: “take the path of the nested data structure [ 'vote' , 'tally' , 'Trainspotting' ] and apply this function. If any keys are missing, then create new arrays instead of them ( Map ). If at the end there is no value, then initialize to zero. " It is this kind of code that allows you to enjoy working with immutable data structures, so it’s worth taking the time to practice this.

4.4.4. Go to the next pair


At the end of the vote on the current pair, go to the next. You need to save the winner and add it to the end of the list of entries so that he will again take part in the voting. The losing entry is simply discarded. In case of a tie, both entries are kept.

Add this logic to the existing implementation 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') })); }); }); 

In our implementation, we simply combine the winners of the current vote with the entries. You can find these winners using the new getWinners function:

 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. Completion of the vote


At some point, we have only one record - the winner, and then the voting ends. And instead of forming a new vote, we explicitly designate this record as the winner in the current state. The end of the vote.

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

In the next implementation, it is necessary to provide for the handling of the situation where, after the next voting is completed, only one position remains in the list of entries:

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

Here you could simply return the Map({winner: entries.first()}) . But instead, we again take the old state and explicitly remove the vote and entries keys from it. This is done with an eye to the future: it may happen that in our state there will be some third-party data that needs to be transmitted in unchanged form using this function. In general, the basis of state transformation functions is a good idea - always convert the old state into a new one, instead of creating a new state from scratch.

Now we have a completely acceptable version of the main logic of our application, expressed in the form of several functions. We also wrote unit tests for them, which were fairly easy for us: no presets or stubs. This is the beauty of pure functions. You can simply call them and check the returned values.

Please note that we have not even installed Redux yet. At the same time, they calmly engaged in the development of application logic, without involving the “framework” in this task. There is something damn nice about it.

4.5. Using Actions and Reducers


So, we have basic functions, but we will not call them directly in Redux. Between the functions and the outside world is a layer of indirect addressing: Actions .

These are simple data structures that describe the changes that should occur with the state of your application. In essence, this is a description of a function call, packed in a small object. By convention, each action has a type attribute that describes which operation this action is for. . , :

 {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. .

Where to begin? 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); } 

That's all! . , . !

: 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 )

Source: https://habr.com/ru/post/303456/


All Articles