mkdir voting-server cd voting-server npm init -y
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
npm install --save-dev mocha chai
mocha
: ./node_modules/mocha/bin/mocha --compilers js:babel-core/register --recursive
package.json
: package.json "scripts": { "test": "mocha --compilers js:babel-core/register --recursive" },
babel-preset-es2015
package that we have already installed. Then just add the "babel"
section to the package.json
: package.json "babel": { "presets": ["es2015"] }
npm
command, we can run our tests: npm run test
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" },
npm install --save immutable npm install --save-dev chai-immutable
test_helper
file: test/test_helper.js import chai from 'chai'; import chaiImmutable from 'chai-immutable'; chai.use(chaiImmutable);
package.json "scripts": { "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive", "test:watch": "npm run test -- --watch" },
42.setValue(43)
. test/immutable_spec.js import {expect} from 'chai'; describe('immutability', () => { describe('a number', () => { function increment(currentState) { return currentState + 1; } it('is immutable', () => { let state = 42; let nextState = increment(state); expect(nextState).to.equal(43); expect(state).to.equal(42); }); }); });
state
does not change when you call increment
, because the numbers are immutable! 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' )); }); }); });
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' ) })); }); }); });
test/immutable_spec.js function addMovie(currentState, movie) { return currentState.update('movies', movies => movies.push(movie)); }
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') })); }); }); });
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); }
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)); }
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.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') })); }); }); });
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) }); }
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() })); }); }); });
src/core.js export function vote(state, entry) { return state.updateIn( ['vote', 'tally', entry], 0, tally => tally + 1 ); }
'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.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
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) }); }
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
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) }); } }
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.Actions
.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);
reducer
): src/reducer.js export default function reducer(state, action) { // , , }
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: [] })); }); });
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; }
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();
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; }
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' })); });
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 ); }
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; }
reduce
. , . , : , , . import {createStore} from 'redux'; const store = createStore(reducer);
store.dispatch({type: 'NEXT'});
store.getState();
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'] })); }); });
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); }
index.js
, Store: index.js import makeStore from './src/store'; export const store = makeStore();
babel-node
), index.js
Store. 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); }
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" },
npm run start
babel-node
babel-cli . Node- Babel-. , , . .startServer
, Redux store : index.js import makeStore from './src/store'; import {startServer} from './src/server'; export const store = makeStore(); startServer(store);
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()) ); }
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()); }); }
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)); }); }
state
.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'});
mkdir voting-client cd voting-client npm init –y
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
. src/index.js console.log('I am alive!');
npm install --save-dev webpack webpack-dev-server
npm install -g webpack webpack-dev-server
. 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
.bundle.js
: webpack
index.js
). webpack-dev-server
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"] }
.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' } };
npm install --save-dev mocha chai
npm install --save-dev jsdom
npm install --save-dev jsdom@3
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;
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]; } });
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" },
npm install --save react react-dom
npm install --save-dev 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.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' ],
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>; } });
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"]} /> ); }); });
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
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>; } });
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>; } });
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') );
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>; } });
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>; } });
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); });
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'); });
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'); });
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'); }); });
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], // ... });
#/
, #/results
— . npm install --save react-router@2.0.0
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') );
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 . , .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> } });
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>; } });
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 }); } });
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>; } });
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'); }); });
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'); });
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>; } });
hasVoted
. , (actions) (reducers), Redux store. ?state
, . , . reducer-a, action, . action : { type: 'SET_STATE', state: { vote: {...} } }
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} } })); }); });
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} } })); });
src/reducer.js import {Map} from 'immutable'; export default function(state = Map(), action) { return state; }
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; }
npm install --save redux
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') );
npm install --save react-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') ); });
App
, .Vote
Winner
, . .Voting
Results
. App
. - store.Voting
. react-redux connect , . , , React-: connect(mapStateToProps)(SomeComponent);
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';
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';
App.jsx
, : src/components/App.jsx import React from 'react'; export default React.createClass({ render: function() { return this.props.children; } });
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') );
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
. , .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' })); });
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} } })); });
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.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') );
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. ! : .VOTE
Redux store.VOTE
reducer- hasVoted
.action
. Redux Store.VOTE
reducer- .VOTE
, Redux stores. . 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. src/remote_action_middleware.js export default store => next => action => { console.log('in middleware', action); return next(action); }
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') );
SET_STATE
, — VOTE
.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);
action
: src/remote_action_middleware.js export default socket => store => next => action => { socket.emit('action', action); return next(action); }
SET_STATE
, . , , SET_STATE
. .{meta: {remote: true}}
: 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 }; }
hasVote
.SET_STATE
.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);
Source: https://habr.com/ru/post/303456/
All Articles