` 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 |
--------------------------------------------------------------------------------