├── .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 |
--------------------------------------------------------------------------------