├── .gitignore ├── nodemon.json ├── .babelrc ├── src ├── timer │ ├── actions.js │ ├── index.js │ ├── saga.js │ ├── components.js │ └── reducer.js └── index.js ├── index.html ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["bundle.js"] 3 | } 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/timer/actions.js: -------------------------------------------------------------------------------- 1 | export const start = () => ({ type: 'START' }) 2 | export const tick = () => ({ type: 'TICK' }) 3 | export const stop = () => ({ type: 'STOP' }) 4 | export const reset = () => ({ type: 'RESET' }) 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Timer App 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | See blog post: http://jaysoo.ca/2016/01/03/managing-processes-in-redux-using-sagas/ 2 | 3 | ## Running 4 | 5 | Install modules: 6 | 7 | ``` 8 | npm install 9 | ``` 10 | 11 | Start server: 12 | 13 | ``` 14 | npm start 15 | ``` 16 | 17 | ## Building 18 | 19 | ``` 20 | npm run build 21 | ``` 22 | 23 | Or 24 | 25 | ``` 26 | npm run watch 27 | ``` 28 | -------------------------------------------------------------------------------- /src/timer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import { Timer } from './components' 5 | import * as actions from './actions' 6 | import { getFormattedTime, getStatus } from './reducer' 7 | 8 | export reducer from './reducer' 9 | export saga from './saga' 10 | 11 | export const View = connect( 12 | state => ({ 13 | time: getFormattedTime(state), 14 | status: getStatus(state) 15 | }), 16 | actions 17 | )(Timer) 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React, { Component} from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { applyMiddleware, createStore } from 'redux' 5 | import createSagaMiddleware from 'redux-saga' 6 | import { Provider } from 'react-redux' 7 | import * as timer from './timer' 8 | 9 | const sagaMiddleware = createSagaMiddleware() 10 | const createStoreWithMiddleware = applyMiddleware(sagaMiddleware)(createStore) 11 | 12 | const store = createStoreWithMiddleware(timer.reducer) 13 | 14 | const Root = () => ( 15 | 16 | 17 | 18 | ) 19 | 20 | sagaMiddleware.run(timer.saga) 21 | 22 | ReactDOM.render(, document.getElementById('root')) 23 | -------------------------------------------------------------------------------- /src/timer/saga.js: -------------------------------------------------------------------------------- 1 | import { actionChannel, call, take, put, race } from 'redux-saga/effects' 2 | import * as actions from './actions' 3 | 4 | // wait :: Number -> Promise 5 | const wait = ms => ( 6 | new Promise(resolve => { 7 | setTimeout(() => resolve(), ms) 8 | }) 9 | ) 10 | 11 | function* runTimer() { 12 | const channel = yield actionChannel('START') 13 | 14 | while(yield take(channel)) { 15 | while(true) { 16 | const winner = yield race({ 17 | stopped: take('STOP'), 18 | tick: call(wait, 1000) 19 | }) 20 | 21 | if (!winner.stopped) { 22 | yield put(actions.tick()) 23 | } else { 24 | break 25 | } 26 | } 27 | } 28 | } 29 | 30 | export default runTimer 31 | 32 | -------------------------------------------------------------------------------- /src/timer/components.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | export const Timer = ({ start, stop, reset, time, status }) => ( 4 |
5 |

6 | { time } ({ status }) 7 |

8 | 13 | 18 | 23 |
24 | ) 25 | 26 | Timer.propTypes = { 27 | start: React.PropTypes.func.isRequired, 28 | stop: React.PropTypes.func.isRequired, 29 | status: PropTypes.string.isRequired, 30 | time: PropTypes.string.isRequired 31 | } 32 | -------------------------------------------------------------------------------- /src/timer/reducer.js: -------------------------------------------------------------------------------- 1 | import { duration } from 'moment' 2 | import { compose, multiply, not, prop } from 'ramda' 3 | 4 | /* Reducer */ 5 | 6 | export default ( 7 | state = { 8 | status: 'Stopped', 9 | seconds: 0 10 | }, action) => { 11 | switch (action.type) { 12 | case 'START': 13 | return { ...state, status: 'Running' } 14 | case 'STOP': 15 | return { ...state, status: 'Stopped' } 16 | case 'TICK': 17 | return { ...state, seconds: state.seconds + 1 } 18 | case 'RESET': 19 | return { ...state, seconds: 0 } 20 | default: 21 | return state 22 | } 23 | } 24 | 25 | /* Selectors */ 26 | 27 | // getFormattedTime :: State -> String 28 | export const getFormattedTime = (state) => formatTime(state.seconds * 1000) 29 | 30 | export const getStatus = (state) => state.status 31 | 32 | /* Private helpers */ 33 | 34 | // pad :: Number -> String 35 | const pad = (t) => t < 10 ? `0${t}` : `${t}` 36 | 37 | // formatMoment :: Moment -> String 38 | const formatMoment = (m) => `${pad(m.minutes())}:${pad(m.seconds())}` 39 | 40 | // formatTime :: Number -> String 41 | const formatTime = compose(formatMoment, duration) 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-redux-saga", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "./node_modules/.bin/browserify src/index.js -t babelify --outfile bundle.js", 7 | "watch": "./node_modules/.bin/nodemon --exec npm run build", 8 | "start": "./node_modules/.bin/http-server" 9 | }, 10 | "author": "Jack Hsu (http://jaysoo.ca/)", 11 | "license": "ISC", 12 | "dependencies": { 13 | "daggy": "0.0.1", 14 | "data.either": "^1.3.0", 15 | "data.maybe": "^1.2.1", 16 | "moment": "^2.10.6", 17 | "pointfree-fantasy": "^0.1.3", 18 | "ramda": "^0.19.0", 19 | "react": "^0.14.5", 20 | "react-dom": "^0.14.5", 21 | "react-redux": "^4.4.5", 22 | "redux": "^3.5.2", 23 | "redux-saga": "^0.10.5", 24 | "reselect": "^2.5.1" 25 | }, 26 | "devDependencies": { 27 | "babel-cli": "^6.3.17", 28 | "babel-polyfill": "^6.3.14", 29 | "babel-preset-es2015": "^6.3.13", 30 | "babel-preset-react": "^6.3.13", 31 | "babel-preset-stage-0": "^6.3.13", 32 | "babelify": "^7.2.0", 33 | "browserify": "^12.0.1", 34 | "http-server": "^0.8.5", 35 | "nodemon": "^1.8.1" 36 | } 37 | } 38 | --------------------------------------------------------------------------------