├── .gitignore ├── .babelrc ├── index.html ├── README.md ├── rootReducer.js ├── DevTools.js ├── configureStore.js ├── ducks ├── clickCounter.js ├── multiplyAll.js └── counter.js ├── index.js ├── components ├── MultiplyAll.js ├── ClickCounter.js ├── AddDynamicCounters.js └── Counter.js ├── server.js ├── LICENSE ├── package.json ├── webpack.config.js └── containers └── App.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .log 3 | .idea 4 | 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "plugins": [ 4 | ["transform-decorators-legacy"], 5 | ["add-module-exports"] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux counter example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-operations-counter-example 2 | An example of solving current redux shortcoming using redux-operations "Prioritized waterfall reducers for predictable, dynamic, multi reducers with a visual API" 3 | 4 | ##Installation 5 | 1. Git clone repo 6 | 2. npm i 7 | 3. npm start 8 | -------------------------------------------------------------------------------- /rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import {counter} from './ducks/counter' 3 | import {clickCounter} from './ducks/clickCounter' 4 | import {multiplyAll} from './ducks/multiplyAll' 5 | 6 | export default combineReducers({ 7 | counter, 8 | clickCounter, 9 | multiplyAll 10 | }); 11 | -------------------------------------------------------------------------------- /DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose } from 'redux' 2 | import {reduxOperations} from 'redux-operations'; 3 | import rootReducer from './rootReducer' 4 | import DevTools from './DevTools'; 5 | 6 | 7 | const enhancer = compose( 8 | reduxOperations(), 9 | DevTools.instrument() 10 | ); 11 | 12 | export default function configureStore(initialState) { 13 | return createStore(rootReducer, initialState, enhancer); 14 | } 15 | -------------------------------------------------------------------------------- /ducks/clickCounter.js: -------------------------------------------------------------------------------- 1 | import {operationReducerFactory} from 'redux-operations'; 2 | import {INCREMENT_COUNTER} from './counter'; 3 | 4 | const defaultState = 0; 5 | export const clickCounter = operationReducerFactory('clickCounter', defaultState, { 6 | INCREMENT_COUNTER: { 7 | resolve: (state, action)=> { 8 | return state + 1; 9 | }, 10 | description: 'Number of times all counters were incremented' 11 | } 12 | }); 13 | 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import Counters from './containers/App' 5 | import configureStore from './configureStore' 6 | import DevTools from './DevTools'; 7 | 8 | const store = configureStore(); 9 | 10 | render( 11 | 12 |
13 | 14 | 15 |
16 |
, 17 | document.getElementById('root') 18 | ) 19 | -------------------------------------------------------------------------------- /components/MultiplyAll.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import {connect} from 'react-redux'; 3 | 4 | const mapStateToProps = (state) => { 5 | return { 6 | result: state.multiplyAll 7 | } 8 | } 9 | 10 | @connect(mapStateToProps) 11 | export default class MultiplyAll extends Component { 12 | render() { 13 | const {result} = this.props 14 | return ( 15 |
16 |

17 | Total multiply: {result} 18 |

19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /components/ClickCounter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import {connect} from 'react-redux'; 3 | 4 | const mapStateToProps = (state) => { 5 | return { 6 | clickCounter: state.clickCounter 7 | } 8 | } 9 | 10 | @connect(mapStateToProps) 11 | export default class ClickCounter extends Component { 12 | render() { 13 | const {clickCounter} = this.props; 14 | return ( 15 |
16 |

17 | Total clicks on increment: {clickCounter} times 18 |

19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ducks/multiplyAll.js: -------------------------------------------------------------------------------- 1 | import {operationReducerFactory} from 'redux-operations'; 2 | import {INCREMENT_COUNTER} from './counter'; 3 | 4 | const initialState = 0; 5 | export const multiplyAll = operationReducerFactory('multiplyAll', initialState, { 6 | INCREMENT_COUNTER: { 7 | //priority must be higher so it comes later than the origination click and update 8 | priority: 100, 9 | resolve: (state, action)=> { 10 | return action.meta.operations.results.counter.state * action.meta.operations.results.clickCounter.state; 11 | }, 12 | description: 'All counters clicked multiplied by value of last counter clicked' 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.get('/randomnumber', function(req, res) { 18 | setTimeout(() => { 19 | const randomNumber = "" + parseInt(Math.random() * 100); 20 | res.send(randomNumber); 21 | }, 1000) 22 | }) 23 | 24 | app.listen(port, function(error) { 25 | if (error) { 26 | console.error(error) 27 | } else { 28 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /components/AddDynamicCounters.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import {walkState} from 'redux-operations'; 3 | import Counter from './Counter'; 4 | import {connect} from 'react-redux'; 5 | 6 | 7 | const mapStateToProps = state => { 8 | return { 9 | numberOfCounters: state.numberOfCounters 10 | } 11 | }; 12 | 13 | @connect(mapStateToProps) 14 | export default class AddDynamicCounters extends Component { 15 | render() { 16 | const { numberOfCounters } = this.props; 17 | const counterArr = []; 18 | for (let i = 0; i < numberOfCounters; i++) { 19 | counterArr.push(i); 20 | } 21 | return ( 22 |
23 | 24 | {numberOfCounters ?

ALL THE COUNTERS

: null} 25 |
{counterArr.map(val => this.renderCounter(val))}
26 |
27 | ) 28 | } 29 | renderCounter = (counterIdx) => { 30 | return ; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Matt Krick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import {walkState, bindOperationToActionCreators} from 'redux-operations'; 3 | import {actionCreators, counter as counterReducer} from '../ducks/counter'; 4 | import {connect} from 'react-redux'; 5 | 6 | const mapStateToProps = (state, props) => { 7 | return { 8 | counter: props.location ? walkState(props.location, state, counterReducer) : state.counter 9 | } 10 | }; 11 | 12 | @connect(mapStateToProps) 13 | export default class Counter extends Component { 14 | render() { 15 | const { location, counter, dispatch} = this.props; 16 | const {increment, decrement, incrementIfOdd, 17 | incrementAsync, setFromFetch, setCounter} = bindOperationToActionCreators(location, counterReducer, actionCreators); 18 | return ( 19 |
20 |

21 | Value: {counter} times 22 | {' '} 23 | 24 | {' '} 25 | 26 | {' '} 27 | 28 | {' '} 29 | 30 | {' '} 31 | 32 | {' '} 33 | 34 | 35 | {' '} 36 |

37 |
38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-counter-example", 3 | "version": "0.0.0", 4 | "description": "Redux counter example", 5 | "scripts": { 6 | "start": "node server.js", 7 | "test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js", 8 | "test:watch": "npm test -- --watch" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rackt/redux.git" 13 | }, 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/rackt/redux/issues" 17 | }, 18 | "homepage": "http://rackt.github.io/redux", 19 | "dependencies": { 20 | "babel-polyfill": "6.3.14", 21 | "babel-runtime": "6.3.19", 22 | "react": "^0.14.0", 23 | "react-dom": "^0.14.0", 24 | "react-redux": "^4.0.0", 25 | "redux": "^3.0.0", 26 | "redux-operations": "^0.5.1", 27 | "redux-thunk": "^0.1.0" 28 | }, 29 | "devDependencies": { 30 | "react-transform-catch-errors": "1.0.1", 31 | "react-transform-hmr": "^1.0.0", 32 | "redbox-react": "1.2.0", 33 | "babel-cli": "6.4.0", 34 | "babel-core": "6.4.0", 35 | "babel-loader": "6.2.1", 36 | "babel-plugin-add-module-exports": "0.1.2", 37 | "babel-plugin-react-transform": "2.0.0", 38 | "babel-plugin-transform-decorators-legacy": "1.3.4", 39 | "babel-preset-es2015": "6.3.13", 40 | "babel-preset-react": "6.3.13", 41 | "babel-preset-stage-0": "6.3.13", 42 | "babel-register": "6.4.3", 43 | "expect": "^1.6.0", 44 | "express": "^4.13.3", 45 | "jsdom": "^5.6.1", 46 | "mocha": "^2.2.5", 47 | "node-libs-browser": "^0.5.2", 48 | "react-addons-test-utils": "^0.14.0", 49 | "webpack": "^1.9.11", 50 | "webpack-dev-middleware": "^1.2.0", 51 | "webpack-hot-middleware": "^2.2.0", 52 | "redux-devtools": "3.0.1", 53 | "redux-devtools-dock-monitor": "1.0.1", 54 | "redux-devtools-log-monitor": "1.0.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | 5 | const babelQuery = { 6 | "plugins": [ 7 | ["transform-decorators-legacy"], 8 | ["react-transform", { 9 | "transforms": [{ 10 | "transform": "react-transform-hmr", 11 | "imports": ["react"], 12 | "locals": ["module"] 13 | }, { 14 | "transform": "react-transform-catch-errors", 15 | "imports": ["react", "redbox-react"] 16 | }] 17 | }] 18 | ] 19 | }; 20 | 21 | module.exports = { 22 | devtool: 'source-map', 23 | entry: [ 24 | 'babel-polyfill', 25 | 'webpack-hot-middleware/client', 26 | './index' 27 | ], 28 | output: { 29 | path: path.join(__dirname, 'dist'), 30 | filename: 'bundle.js', 31 | publicPath: '/static/' 32 | }, 33 | plugins: [ 34 | new webpack.optimize.OccurenceOrderPlugin(), 35 | new webpack.HotModuleReplacementPlugin(), 36 | new webpack.NoErrorsPlugin(), 37 | new webpack.DefinePlugin({ 38 | 'process.env.NODE_ENV': JSON.stringify('development') 39 | }) 40 | ], 41 | module: { 42 | loaders: [ 43 | { 44 | test: /\.js$/, 45 | loader: 'babel', 46 | exclude: /node_modules/, 47 | query: babelQuery, 48 | include: __dirname 49 | } 50 | ] 51 | } 52 | } 53 | 54 | 55 | // When inside Redux repo, prefer src to compiled version. 56 | // You can safely delete these lines in your project. 57 | var reduxSrc = path.join(__dirname, '..', '..', 'src'); 58 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules'); 59 | var fs = require('fs'); 60 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) { 61 | // Resolve Redux to source 62 | module.exports.resolve = { alias: { 'redux': reduxSrc } } 63 | // Compile Redux from source 64 | module.exports.module.loaders.push({ 65 | test: /\.js$/, 66 | loaders: [ 'babel' ], 67 | include: reduxSrc 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /containers/App.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux' 2 | import { connect } from 'react-redux' 3 | import Counter from '../components/Counter' 4 | import ClickCounter from '../components/ClickCounter' 5 | import MultiplyAll from '../components/MultiplyAll' 6 | import AddDynamicCounters from '../components/AddDynamicCounters' 7 | import React, { Component, PropTypes } from 'react' 8 | 9 | const topCounter = ['counters', 'top']; 10 | const bottomCounter = ['counters', 'bottom']; 11 | 12 | export default class Counters extends Component { 13 | render() { 14 | return ( 15 |
16 |
17 | Explore the api in devtools to see operation flow, args, and descriptions ----> 18 |
Your normal state is under userState
19 |
20 |
21 |

1. Plain counter

22 | 23 |
24 |
25 |

2. Top/Bottom counters

26 |

(Solves #2 from https://github.com/evancz/elm-architecture-tutorial)

27 | 28 | 29 |
30 |
31 |

3. Number of times all counters were incremented

32 |

(Solves http://blog.javascripting.com/2016/02/02/encapsulation-in-redux/)

33 | 34 |
35 |
36 |

4. All counters clicked multiplied by value of last incremented counter clicked

37 |

(Solves https://github.com/reactjs/redux/issues/1315#issue-129937015)

38 | 39 |
40 |
41 |

5. A counter that creates counters

42 |

(Solves #3, #4 from https://github.com/evancz/elm-architecture-tutorial)

43 | 44 |
45 |
46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ducks/counter.js: -------------------------------------------------------------------------------- 1 | import {operationReducerFactory, bindOperationToActionCreators} from 'redux-operations'; 2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; 3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; 4 | export const INCREMENT_ASYNC = 'INCREMENT_ASYNC'; 5 | export const INCREMENT_IF_ODD = 'INCREMENT_IF_ODD'; 6 | export const SET_COUNTER = 'SET_COUNTER'; 7 | export const FETCH_RANDOM_REQUEST = 'FETCH_RANDOM_REQUEST'; 8 | 9 | export function increment() { 10 | return { 11 | type: INCREMENT_COUNTER 12 | } 13 | } 14 | 15 | export function decrement() { 16 | return { 17 | type: DECREMENT_COUNTER 18 | } 19 | } 20 | 21 | export function incrementIfOdd() { 22 | return { 23 | type: INCREMENT_IF_ODD 24 | } 25 | } 26 | 27 | export function incrementAsync() { 28 | return { 29 | type: INCREMENT_ASYNC 30 | } 31 | } 32 | 33 | export function setFromFetch() { 34 | return { 35 | type: FETCH_RANDOM_REQUEST 36 | } 37 | } 38 | 39 | export function setCounter(newValue) { 40 | return { 41 | type: SET_COUNTER, 42 | payload: {newValue: +newValue} 43 | } 44 | } 45 | 46 | export const actionCreators = {increment, decrement, incrementIfOdd, incrementAsync, setFromFetch, setCounter}; 47 | 48 | const initialState = 0; 49 | export const counter = operationReducerFactory('counter', initialState, { 50 | INCREMENT_COUNTER: { 51 | resolve: (state, action)=> state + 1 52 | }, 53 | DECREMENT_COUNTER: { 54 | resolve: (state, action)=> state - 1 55 | }, 56 | INCREMENT_IF_ODD: { 57 | resolve: (state, action) => state % 2 ? state + 1 : state 58 | }, 59 | INCREMENT_ASYNC: { 60 | resolve: (state, action)=> { 61 | setTimeout(()=> { 62 | const {dispatch, locationInState} = action.meta.operations; 63 | const inc = bindOperationToActionCreators(locationInState, counter, increment); 64 | dispatch(inc()); 65 | }, 1000); 66 | return state; 67 | } 68 | }, 69 | SET_COUNTER: { 70 | resolve: (state, action) => action.payload.newValue, 71 | arguments: { 72 | newValue: {type: Number, description: 'The new value for the counter'} 73 | } 74 | }, 75 | FETCH_RANDOM_REQUEST: { 76 | priority: 1, 77 | resolve: (state, action)=> { 78 | const {dispatch, locationInState} = action.meta.operations; 79 | const set = bindOperationToActionCreators(locationInState, counter, setCounter); 80 | window.fetch('http://localhost:3000/randomnumber') 81 | .then(res => { 82 | return res.text() 83 | }) 84 | .then(data => { 85 | dispatch(set(parseInt(data))); 86 | }) 87 | .catch(err => { 88 | console.log('ERR', err) 89 | }); 90 | return state; 91 | } 92 | } 93 | }); 94 | 95 | --------------------------------------------------------------------------------