├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── example ├── index.html └── index.js ├── lib └── index.js ├── package.json ├── src └── index.js ├── tests ├── index.js └── karma.conf.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-rackt", 3 | "rules": { 4 | "valid-jsdoc": 2, 5 | "react/jsx-uses-react": 1, 6 | "react/jsx-no-undef": 2, 7 | "react/jsx-uses-vars": 2, 8 | "react/wrap-multilines": 2 9 | }, 10 | "plugins": [ 11 | "react" 12 | ] 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # osx noise 2 | .DS_Store 3 | profile 4 | 5 | # xcode noise 6 | build/* 7 | *.mode1 8 | *.mode1v3 9 | *.mode2v3 10 | *.perspective 11 | *.perspectivev3 12 | *.pbxuser 13 | *.xcworkspace 14 | xcuserdata 15 | 16 | # svn & cvs 17 | .svn 18 | CVS 19 | node_modules 20 | tests/coverage 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-redux-saga 2 | --- 3 | 4 | react bindings for [redux-saga](https://github.com/yelouafi/redux-saga/) 5 | 6 | kanyewestiskanyebest 7 | 8 | ```jsx 9 | import {Saga} from 'react-redux-saga'; 10 | 11 | // Saga as a component 12 | // starts running once it mounts 13 | // gets cancelled when it unmounts 14 | 15 | 16 | // you can read redux state / passed props 17 | function* run(getState, {x, y}){ 18 | // getState().key.state... 19 | // x === 1 20 | // y === 2 21 | } 22 | 23 | 24 | 25 | // alternately, there's a decorator version 26 | @saga(function*(getState, props){ 27 | // happy, @sebastienlorber? 28 | }) 29 | class App extends Component{ 30 | render(){ 31 | // ... 32 | } 33 | } 34 | 35 | ``` 36 | 37 | getting started 38 | --- 39 | 40 | include `` high up in your react tree, and pass it the result of `createSagaMiddleware`. example - 41 | 42 | ```jsx 43 | let middle = createSagaMiddleware(); 44 | let store = createStore(/* reducer */, applyMiddleware(/* ... */, middle)); // from redux 45 | 46 | render( /* react-dom, etc */ 47 | 48 | , dom); 49 | } 50 | 51 | ``` 52 | 53 | etc 54 | --- 55 | 56 | - from the work on [redux-react-local](https://github.com/threepointone/redux-react-local) 57 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | react-redux-saga 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { render } from 'react-dom' 4 | import { createStore, combineReducers, applyMiddleware } from 'redux' 5 | import { Sagas, Saga } from '../src' 6 | import { Provider } from 'react-redux' 7 | import createSagaMiddleware from 'redux-saga' 8 | import { cps } from 'redux-saga/effects' 9 | 10 | const TRUE = true // whetever rackt 11 | 12 | function sleep(period, done) { 13 | setTimeout(() => done(null, true), period) 14 | } 15 | 16 | function *run(_, { callback }) { 17 | while(TRUE) { 18 | yield cps(sleep, 1000) 19 | callback() 20 | } 21 | } 22 | 23 | class App extends Component { 24 | state = { x: 0 }; 25 | onNext = () => { 26 | this.setState({ 27 | x: this.state.x + 1 28 | }) 29 | } 30 | render() { 31 | return (
32 | 33 | {this.state.x} 34 |
) 35 | } 36 | } 37 | 38 | const reducers = { 39 | x: (x = {}) => x 40 | } 41 | 42 | class SagaRoot extends Component { 43 | sagaMiddleware = createSagaMiddleware(); 44 | store = createStore(combineReducers(reducers), {}, applyMiddleware(this.sagaMiddleware)); 45 | 46 | render() { 47 | return ( 48 | 49 | {this.props.children} 50 | 51 | ) 52 | } 53 | } 54 | 55 | render(, document.getElementById('app')) 56 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.Saga = exports.Sagas = undefined; 7 | 8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 9 | 10 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 11 | 12 | exports.saga = saga; 13 | 14 | var _react = require('react'); 15 | 16 | var _react2 = _interopRequireDefault(_react); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 19 | 20 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 21 | 22 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 23 | 24 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 25 | 26 | // top level component 27 | 28 | var Sagas = exports.Sagas = function (_Component) { 29 | _inherits(Sagas, _Component); 30 | 31 | function Sagas() { 32 | _classCallCheck(this, Sagas); 33 | 34 | return _possibleConstructorReturn(this, Object.getPrototypeOf(Sagas).apply(this, arguments)); 35 | } 36 | 37 | _createClass(Sagas, [{ 38 | key: 'getChildContext', 39 | value: function getChildContext() { 40 | return { 41 | sagas: this.props.middleware 42 | }; 43 | } 44 | }, { 45 | key: 'render', 46 | value: function render() { 47 | return _react.Children.only(this.props.children); 48 | } 49 | }]); 50 | 51 | return Sagas; 52 | }(_react.Component); 53 | 54 | // 55 | // simple! 56 | 57 | 58 | Sagas.propTypes = { 59 | // as returned from redux-saga:createSagaMiddleware 60 | middleware: _react.PropTypes.func.isRequired 61 | }; 62 | Sagas.childContextTypes = { 63 | sagas: _react.PropTypes.func.isRequired 64 | }; 65 | 66 | var Saga = exports.Saga = function (_Component2) { 67 | _inherits(Saga, _Component2); 68 | 69 | function Saga() { 70 | _classCallCheck(this, Saga); 71 | 72 | return _possibleConstructorReturn(this, Object.getPrototypeOf(Saga).apply(this, arguments)); 73 | } 74 | 75 | _createClass(Saga, [{ 76 | key: 'componentDidMount', 77 | value: function componentDidMount() { 78 | if (!this.context.sagas) { 79 | throw new Error('did you forget to include ?'); 80 | } 81 | this.runningSaga = this.context.sagas.run(this.props.saga, this.props); 82 | } // todo - test fpr generator 83 | 84 | }, { 85 | key: 'componentWillReceiveProps', 86 | value: function componentWillReceiveProps() { 87 | // ?? 88 | } 89 | }, { 90 | key: 'render', 91 | value: function render() { 92 | return !this.props.children ? null : _react.Children.only(this.props.children); 93 | } 94 | }, { 95 | key: 'componentWillUnmount', 96 | value: function componentWillUnmount() { 97 | if (this.runningSaga) { 98 | this.runningSaga.cancel(); 99 | delete this.runningSaga; 100 | } 101 | } 102 | }]); 103 | 104 | return Saga; 105 | }(_react.Component); 106 | 107 | // decorator version 108 | 109 | 110 | Saga.propTypes = { 111 | saga: _react.PropTypes.func.isRequired }; 112 | Saga.contextTypes = { 113 | sagas: _react.PropTypes.func.isRequired 114 | }; 115 | function saga(run) { 116 | return function (Target) { 117 | var _class, _temp; 118 | 119 | return _temp = _class = function (_Component3) { 120 | _inherits(SagaDecorator, _Component3); 121 | 122 | function SagaDecorator() { 123 | _classCallCheck(this, SagaDecorator); 124 | 125 | return _possibleConstructorReturn(this, Object.getPrototypeOf(SagaDecorator).apply(this, arguments)); 126 | } 127 | 128 | _createClass(SagaDecorator, [{ 129 | key: 'render', 130 | value: function render() { 131 | return _react2.default.createElement( 132 | Saga, 133 | _extends({ saga: run }, this.props), 134 | _react2.default.createElement(Target, this.props) 135 | ); 136 | } 137 | }]); 138 | 139 | return SagaDecorator; 140 | }(_react.Component), _class.displayName = 'saga:' + (Target.displayName || Target.name), _temp; 141 | }; 142 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-saga", 3 | "version": "1.0.2", 4 | "description": "react bindings for redux-saga", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "NODE_ENV=production babel src -d lib", 8 | "start": "./node_modules/.bin/webpack-dev-server --hot --inline --port 3000 --config webpack.config.js", 9 | "test": "NODE_ENV=test ./node_modules/.bin/babel-node ./node_modules/.bin/karma start tests/karma.conf.js --single-run" 10 | }, 11 | "author": "Sunil Pai ", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "babel": "^6.5.2", 15 | "babel-cli": "^6.5.1", 16 | "babel-core": "^6.5.2", 17 | "babel-eslint": "^4.1.8", 18 | "babel-loader": "^6.2.2", 19 | "babel-preset-es2015": "^6.5.0", 20 | "babel-preset-react": "^6.5.0", 21 | "babel-preset-react-hmre": "^1.1.0", 22 | "babel-preset-stage-0": "^6.5.0", 23 | "eslint": "^2.0.0", 24 | "eslint-config-defaults": "^9.0.0", 25 | "eslint-config-rackt": "^1.1.1", 26 | "eslint-plugin-filenames": "^0.2.0", 27 | "eslint-plugin-react": "^3.16.1", 28 | "expect": "^1.14.0", 29 | "expect-jsx": "^2.3.0", 30 | "isparta-loader": "^2.0.0", 31 | "karma": "^0.13.21", 32 | "karma-chrome-launcher": "^0.2.2", 33 | "karma-coverage": "^0.5.3", 34 | "karma-expect": "^1.1.1", 35 | "karma-mocha": "^0.2.2", 36 | "karma-mocha-reporter": "^1.1.5", 37 | "karma-webpack": "^1.7.0", 38 | "mocha": "^2.4.5", 39 | "react": "^0.14.7", 40 | "react-dom": "^0.14.7", 41 | "react-redux": "^4.4.0", 42 | "redux": "^3.3.1", 43 | "redux-saga": "^0.8.1", 44 | "webpack": "^1.12.13", 45 | "webpack-dev-server": "^1.14.1" 46 | }, 47 | "peerDependencies": { 48 | "react": "*", 49 | "redux-saga": "*" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Children, Component } from 'react' 2 | 3 | // top level component 4 | export class Sagas extends Component { 5 | static propTypes = { 6 | // as returned from redux-saga:createSagaMiddleware 7 | middleware: PropTypes.func.isRequired 8 | }; 9 | static childContextTypes = { 10 | sagas: PropTypes.func.isRequired 11 | }; 12 | getChildContext() { 13 | return { 14 | sagas: this.props.middleware 15 | } 16 | } 17 | render() { 18 | return Children.only(this.props.children) 19 | } 20 | } 21 | 22 | // 23 | // simple! 24 | export class Saga extends Component { 25 | static propTypes = { 26 | saga: PropTypes.func.isRequired // todo - test fpr generator 27 | }; 28 | static contextTypes = { 29 | sagas: PropTypes.func.isRequired 30 | }; 31 | 32 | componentDidMount() { 33 | if(!this.context.sagas) { 34 | throw new Error('did you forget to include ?') 35 | } 36 | this.runningSaga = this.context.sagas.run(this.props.saga, this.props) 37 | } 38 | 39 | componentWillReceiveProps() { 40 | // ?? 41 | } 42 | render() { 43 | return !this.props.children ? null : Children.only(this.props.children) 44 | } 45 | componentWillUnmount() { 46 | if(this.runningSaga) { 47 | this.runningSaga.cancel() 48 | delete this.runningSaga 49 | } 50 | 51 | } 52 | } 53 | 54 | // decorator version 55 | export function saga(run) { 56 | return function (Target) { 57 | return class SagaDecorator extends Component { 58 | static displayName = 'saga:' + (Target.displayName || Target.name) 59 | render() { 60 | return ( 61 | 62 | ) 63 | } 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach, afterEach */ 2 | import React, { Component } from 'react' 3 | import { Sagas, Saga, saga } from '../src/' 4 | import { createStore, applyMiddleware, combineReducers } from 'redux' 5 | import { Provider } from 'react-redux' 6 | import { render, unmountComponentAtNode } from 'react-dom' 7 | import createSagaMiddleware, { isCancelError } from 'redux-saga' 8 | 9 | import { cps } from 'redux-saga/effects' 10 | 11 | const TRUE = true // satisfying eslint while(true) 12 | 13 | import expect from 'expect' 14 | import expectJSX from 'expect-jsx' 15 | expect.extend(expectJSX) 16 | 17 | function sleep(period, done) { 18 | setTimeout(() => done(null, true), period) 19 | } 20 | 21 | class SagaRoot extends Component { 22 | sagaMiddleware = createSagaMiddleware() 23 | store = createStore( 24 | combineReducers(this.props.reducers || { x: (x = {}) => x }), 25 | applyMiddleware(this.sagaMiddleware) 26 | ) 27 | render() { 28 | return ( 29 | 30 | {this.props.children} 31 | 32 | ) 33 | } 34 | } 35 | 36 | describe('react-redux-saga', () => { 37 | let node 38 | beforeEach(() => node = document.createElement('div')) 39 | afterEach(() => unmountComponentAtNode(node)) 40 | 41 | it('should throw when you don\'t include ', () => { 42 | let run = function*() {} 43 | expect(() => render(, node)).toThrow() 44 | }) 45 | 46 | // sagas 47 | it('accepts a saga', done => { 48 | let started = false 49 | 50 | let run = function*() { 51 | started = true 52 | yield cps(sleep, 300) 53 | done() 54 | } 55 | 56 | expect(started).toEqual(false) 57 | 58 | render(, node) 59 | expect(started).toEqual(true) 60 | }) 61 | 62 | it('starts when the component is mounted', () => { 63 | // as above 64 | }) 65 | 66 | it('gets cancelled when the component unmounts', done => { 67 | let unmounted = false 68 | 69 | let run = function*() { 70 | try { 71 | while (TRUE) { 72 | yield cps(sleep, 100) 73 | } 74 | } 75 | catch (e) { 76 | if (isCancelError(e) && unmounted === true) { 77 | done() 78 | } 79 | } 80 | } 81 | 82 | 83 | render(, node) 84 | 85 | sleep(500, () => { 86 | unmounted = true 87 | unmountComponentAtNode(node) 88 | }) 89 | 90 | 91 | }) 92 | 93 | it('can receive props', done => { 94 | let run = function*(_, { x }) { 95 | expect(x).toEqual(123) 96 | done() 97 | } 98 | 99 | render(, node) 100 | }) 101 | 102 | it('can read from global redux state', done => { 103 | let run = function *(getState) { 104 | expect(getState().x.a).toEqual(123) 105 | done() 106 | } 107 | 108 | render( state }}> 109 | 110 | , node) 111 | 112 | }) 113 | 114 | it('decorator version', () => { 115 | 116 | let App = saga(function*(getState, { x }) { 117 | expect(getState().x).toEqual({ a: 123 }) 118 | expect(x).toEqual(123) 119 | 120 | })( 121 | class App extends Component { 122 | render() { 123 | return null 124 | } 125 | }) 126 | 127 | render( state }}> 128 | 129 | , node) 130 | 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config){ 2 | config.set({ 3 | browsers: ['Chrome'], 4 | files: ['../node_modules/babel-polyfill/dist/polyfill.js', 5 | './index.js' 6 | ], 7 | reporters: ['mocha', 'coverage'], 8 | mochaReporter: { 9 | output: 'autowatch' 10 | }, 11 | preprocessors: { 12 | '../src/*.js': ['coverage'], 13 | './*.js': ['webpack'], 14 | }, 15 | webpack: { 16 | module: { 17 | loaders: [{ 18 | test: /\.js$/, 19 | exclude: /node_modules/, 20 | loader: 'babel' 21 | }, { 22 | test: /\.js$/, 23 | include: require('path').join(__dirname, '../src'), 24 | loader: 'isparta' 25 | }] 26 | } 27 | }, 28 | webpackMiddleware: { 29 | noInfo: true 30 | }, 31 | frameworks: ['mocha', 'expect'] 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | module.exports = { 3 | entry: { 4 | app: [ 'babel-polyfill', './example/index.js' ] 5 | }, 6 | output: { 7 | path: path.join(__dirname, './example'), 8 | publicPath: '/example', 9 | filename: 'app.js' 10 | }, 11 | module: { 12 | loaders: [ 13 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }, 14 | { test: /\.css$/, loader: 'style-loader!css-loader?modules' } 15 | ] 16 | } 17 | } 18 | --------------------------------------------------------------------------------