├── .babelrc
├── .gitignore
├── examples
├── auth
│ ├── .babelrc
│ ├── dev
│ │ └── index.html
│ ├── src
│ │ ├── constants
│ │ │ └── actions.js
│ │ ├── main.js
│ │ ├── actions
│ │ │ └── actionCreators.js
│ │ ├── reducers
│ │ │ └── rootReducer.js
│ │ ├── components
│ │ │ └── Application.jsx
│ │ └── sagas
│ │ │ └── authSaga.js
│ ├── webpack.config.js
│ └── package.json
├── real-world
│ ├── .babelrc
│ ├── src
│ │ ├── containers
│ │ │ ├── Root.js
│ │ │ ├── DevTools.js
│ │ │ ├── Root.prod.js
│ │ │ ├── Root.dev.js
│ │ │ ├── App.js
│ │ │ ├── UserPage.js
│ │ │ └── RepoPage.js
│ │ ├── store
│ │ │ ├── configureStore.js
│ │ │ ├── configureStore.prod.js
│ │ │ └── configureStore.dev.js
│ │ ├── routes.js
│ │ ├── components
│ │ │ ├── User.jsx
│ │ │ ├── Repo.jsx
│ │ │ ├── List.jsx
│ │ │ └── Explore.jsx
│ │ ├── main.js
│ │ ├── actions
│ │ │ └── index.js
│ │ ├── reducers
│ │ │ ├── index.js
│ │ │ └── paginate.js
│ │ └── sagas
│ │ │ ├── api.js
│ │ │ └── realWorldSaga.js
│ ├── dev
│ │ └── index.html
│ ├── webpack.config.js
│ └── package.json
└── undo-redo-optimistic
│ ├── .babelrc
│ ├── dev
│ └── index.html
│ ├── src
│ ├── constants
│ │ └── actions.js
│ ├── main.js
│ ├── actions
│ │ └── actionCreators.js
│ ├── components
│ │ └── Application.jsx
│ ├── reducers
│ │ └── rootReducer.js
│ └── sagas
│ │ └── commandSaga.js
│ ├── webpack.config.js
│ └── package.json
├── docs
├── atm_1.png
└── atm_2.png
├── .travis.yml
├── src
└── index.js
├── package.json
├── test
└── sagaMiddleware.test.js
├── README.md
└── .eslintrc
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | lib
4 | npm-debug.log
--------------------------------------------------------------------------------
/examples/auth/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2", "react"]
3 | }
--------------------------------------------------------------------------------
/examples/real-world/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2", "react"]
3 | }
--------------------------------------------------------------------------------
/docs/atm_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salsita/redux-saga-rxjs/HEAD/docs/atm_1.png
--------------------------------------------------------------------------------
/docs/atm_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salsita/redux-saga-rxjs/HEAD/docs/atm_2.png
--------------------------------------------------------------------------------
/examples/undo-redo-optimistic/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2", "react"]
3 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "5"
4 | script:
5 | - npm run lint
6 | - npm test
--------------------------------------------------------------------------------
/examples/real-world/src/containers/Root.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./Root.prod');
3 | } else {
4 | module.exports = require('./Root.dev');
5 | }
6 |
--------------------------------------------------------------------------------
/examples/real-world/src/store/configureStore.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./configureStore.prod');
3 | } else {
4 | module.exports = require('./configureStore.dev');
5 | }
6 |
--------------------------------------------------------------------------------
/examples/auth/dev/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | redux-saga-rxjs
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/real-world/dev/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | redux-saga-rxjs
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/undo-redo-optimistic/dev/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | redux-saga-rxjs
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/auth/src/constants/actions.js:
--------------------------------------------------------------------------------
1 | // User actions
2 | export const LOG_IN = 'LOG_IN';
3 | export const LOG_OUT = 'LOG_OUT';
4 | export const CHANGE_CREDENTIALS = 'CHANGE_CREDENTIALS';
5 |
6 | // Saga actions
7 | export const TOKEN_REFRESHED = 'TOKEN_REFRESHED';
8 | export const HIDE_TOAST = 'HIDE_TOAST';
9 |
10 | // API callbacks
11 | export const LOGGED_IN = 'LOGGED_IN';
12 | export const LOG_IN_FAILURE = 'LOG_IN_FAILURE';
13 |
--------------------------------------------------------------------------------
/examples/real-world/src/containers/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 |
--------------------------------------------------------------------------------
/examples/real-world/src/store/configureStore.prod.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import sagaMiddleware from 'redux-saga-rxjs';
3 | import rootReducer from '../reducers';
4 | import realWorldSaga from '../sagas/realWorldSaga';
5 |
6 | export default function configureStore(initialState) {
7 | return createStore(
8 | rootReducer,
9 | initialState,
10 | applyMiddleware(sagaMiddleware(realWorldSaga))
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/examples/real-world/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route } from 'react-router';
3 | import App from './containers/App';
4 | import UserPage from './containers/UserPage';
5 | import RepoPage from './containers/RepoPage';
6 |
7 | export default (
8 |
9 |
11 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/examples/real-world/src/components/User.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 |
4 | export default ({ user }) => {
5 | const { login, avatarUrl, name } = user;
6 |
7 | return (
8 |
9 |
10 |

11 |
12 | {login} {name && ({name})}
13 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/examples/real-world/src/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { hashHistory } from 'react-router';
4 | import { syncHistoryWithStore } from 'react-router-redux';
5 |
6 | import configureStore from './store/configureStore';
7 | import Root from './containers/Root';
8 |
9 | const store = configureStore();
10 | const history = syncHistoryWithStore(hashHistory, store);
11 |
12 | render(, document.getElementById('app'));
13 |
--------------------------------------------------------------------------------
/examples/real-world/src/containers/Root.prod.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Provider } from 'react-redux';
3 | import routes from '../routes';
4 | import { Router } from 'react-router';
5 |
6 | export default class Root extends Component {
7 | render() {
8 | const { store, history } = this.props;
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | Root.propTypes = {
18 | store: PropTypes.object.isRequired
19 | };
20 |
--------------------------------------------------------------------------------
/examples/undo-redo-optimistic/src/constants/actions.js:
--------------------------------------------------------------------------------
1 | // User actions
2 | export const CHANGE_TEXT = 'CHANGE_TEXT';
3 | export const ADD_TODO = 'ADD_TODO';
4 | export const UNDO = 'UNDO';
5 | export const REDO = 'REDO';
6 |
7 | // Saga actions
8 | export const ADD_TODO_ID = 'ADD_TODO_ID';
9 | export const CLEAR_REDO_LOG = 'CLEAR_REDO_LOG';
10 |
11 | // API callbacks
12 | export const TODO_ADDED = 'TODO_ADDED';
13 | export const ADD_TODO_FAILED = 'ADD_TODO_FAILED';
14 | export const UNDONE = 'UNDONE';
15 | export const UNDO_FAILED = 'UNDO_FAILED';
16 | export const REDONE = 'REDONE';
17 |
--------------------------------------------------------------------------------
/examples/real-world/src/components/Repo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 |
4 | export default ({ owner, repo }) => {
5 | const { login } = owner;
6 | const { name, description } = repo;
7 |
8 | return (
9 |
10 |
11 |
12 | {name}
13 |
14 | {' by '}
15 |
16 | {login}
17 |
18 |
19 | {description &&
20 |
{description}
21 | }
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/examples/auth/src/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { createStore, applyMiddleware } from 'redux';
4 | import { Provider } from 'react-redux';
5 | import sagaMiddleware from 'redux-saga-rxjs';
6 |
7 | import Application from './components/Application';
8 | import rootReducer from './reducers/rootReducer';
9 | import authSaga from './sagas/authSaga';
10 |
11 | const store = createStore(rootReducer, undefined, applyMiddleware(sagaMiddleware(authSaga)));
12 |
13 | render((
14 |
15 |
16 |
17 | ), document.getElementById('app'));
18 |
--------------------------------------------------------------------------------
/examples/undo-redo-optimistic/src/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { createStore, applyMiddleware } from 'redux';
4 | import { Provider } from 'react-redux';
5 | import sagaMiddleware from 'redux-saga-rxjs';
6 |
7 | import Application from './components/Application';
8 | import rootReducer from './reducers/rootReducer';
9 | import commandSaga from './sagas/commandSaga';
10 |
11 | const store = createStore(rootReducer, undefined, applyMiddleware(sagaMiddleware(commandSaga)));
12 |
13 | render((
14 |
15 |
16 |
17 | ), document.getElementById('app'));
18 |
--------------------------------------------------------------------------------
/examples/real-world/src/containers/Root.dev.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Provider } from 'react-redux';
3 | import routes from '../routes';
4 | import DevTools from './DevTools';
5 | import { Router } from 'react-router';
6 |
7 | export default class Root extends Component {
8 | render() {
9 | const { store, history } = this.props;
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 | }
20 |
21 | Root.propTypes = {
22 | store: PropTypes.object.isRequired
23 | };
24 |
--------------------------------------------------------------------------------
/examples/auth/src/actions/actionCreators.js:
--------------------------------------------------------------------------------
1 | import * as Actions from '../constants/actions';
2 |
3 | export const logIn = credentials => ({type: Actions.LOG_IN, payload: credentials});
4 | export const logOut = () => ({type: Actions.LOG_OUT, payload: null});
5 | export const changeCredentials = credentials => ({type: Actions.CHANGE_CREDENTIALS, payload: credentials});
6 | export const tokenRefreshed = refreshed => ({type: Actions.TOKEN_REFRESHED, payload: refreshed});
7 | export const hideToast = () => ({type: Actions.HIDE_TOAST, payload: null});
8 | export const loggedIn = (credentials, refreshed) => ({type: Actions.LOGGED_IN, payload: { credentials, refreshed }});
9 | export const logInFailure = () => ({type: Actions.LOG_IN_FAILURE, payload: null});
10 |
--------------------------------------------------------------------------------
/examples/auth/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | debug: true,
6 | target: 'web',
7 | devtool: 'sourcemap',
8 | plugins: [
9 | new webpack.NoErrorsPlugin()
10 | ],
11 | entry: [
12 | 'webpack-dev-server/client?http://localhost:3000',
13 | 'webpack/hot/only-dev-server',
14 | './src/main.js'
15 | ],
16 | output: {
17 | path: path.join(__dirname, './dev'),
18 | filename: 'app.bundle.js'
19 | },
20 | module: {
21 | loaders: [{
22 | test: /\.jsx$|\.js$/,
23 | loaders: ['babel-loader'],
24 | include: path.join(__dirname, './src')
25 | }]
26 | },
27 | resolve: {
28 | extensions: ['', '.js', '.jsx']
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/examples/real-world/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | debug: true,
6 | target: 'web',
7 | devtool: 'sourcemap',
8 | plugins: [
9 | new webpack.NoErrorsPlugin()
10 | ],
11 | entry: [
12 | 'webpack-dev-server/client?http://localhost:3000',
13 | 'webpack/hot/only-dev-server',
14 | './src/main.js'
15 | ],
16 | output: {
17 | path: path.join(__dirname, './dev'),
18 | filename: 'app.bundle.js'
19 | },
20 | module: {
21 | loaders: [{
22 | test: /\.jsx$|\.js$/,
23 | loaders: ['babel-loader'],
24 | include: path.join(__dirname, './src')
25 | }]
26 | },
27 | resolve: {
28 | extensions: ['', '.js', '.jsx']
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/examples/undo-redo-optimistic/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | debug: true,
6 | target: 'web',
7 | devtool: 'sourcemap',
8 | plugins: [
9 | new webpack.NoErrorsPlugin()
10 | ],
11 | entry: [
12 | 'webpack-dev-server/client?http://localhost:3000',
13 | 'webpack/hot/only-dev-server',
14 | './src/main.js'
15 | ],
16 | output: {
17 | path: path.join(__dirname, './dev'),
18 | filename: 'app.bundle.js'
19 | },
20 | module: {
21 | loaders: [{
22 | test: /\.jsx$|\.js$/,
23 | loaders: ['babel-loader'],
24 | include: path.join(__dirname, './src')
25 | }]
26 | },
27 | resolve: {
28 | extensions: ['', '.js', '.jsx']
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/examples/real-world/src/components/List.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const renderLoadMore = (isFetching, onLoadMoreClick) => (
4 |
9 | );
10 |
11 | export default ({ isFetching, nextPageUrl, pageCount, items, renderItem, loadingLabel, onLoadMoreClick }) => {
12 | const isEmpty = items.length === 0;
13 | if (isEmpty && isFetching) {
14 | return {loadingLabel}
;
15 | }
16 |
17 | const isLastPage = !nextPageUrl;
18 | if (isEmpty && isLastPage) {
19 | return Nothing here!
;
20 | }
21 |
22 | return (
23 |
24 | {items.map(renderItem)}
25 | {pageCount > 0 && !isLastPage && renderLoadMore(isFetching, onLoadMoreClick)}
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/examples/real-world/src/store/configureStore.dev.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import createLogger from 'redux-logger';
3 | import sagaMiddleware from 'redux-saga-rxjs';
4 | import rootReducer from '../reducers';
5 | import DevTools from '../containers/DevTools';
6 | import realWorldSaga from '../sagas/realWorldSaga';
7 |
8 | export default function configureStore(initialState) {
9 | const store = createStore(
10 | rootReducer,
11 | initialState,
12 | compose(
13 | applyMiddleware(createLogger(), sagaMiddleware(realWorldSaga)),
14 | DevTools.instrument()
15 | )
16 | );
17 |
18 | if (module.hot) {
19 | // Enable Webpack hot module replacement for reducers
20 | module.hot.accept('../reducers', () => {
21 | const nextRootReducer = require('../reducers').default;
22 | store.replaceReducer(nextRootReducer);
23 | });
24 | }
25 |
26 | return store;
27 | }
28 |
--------------------------------------------------------------------------------
/examples/auth/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-saga-rxjs-example-auth",
3 | "version": "0.1.0",
4 | "scripts": {
5 | "start": "./node_modules/.bin/webpack-dev-server --config webpack.config.js --port 3000 --hot --content-base ./dev"
6 | },
7 | "devDependencies": {
8 | "babel-cli": "^6.5.1",
9 | "babel-core": "^6.5.2",
10 | "babel-eslint": "^4.1.8",
11 | "babel-loader": "^6.2.2",
12 | "babel-preset-es2015": "^6.5.0",
13 | "babel-preset-react": "^6.5.0",
14 | "babel-preset-stage-2": "^6.5.0",
15 | "webpack": "^1.12.4",
16 | "webpack-dev-server": "^1.12.1"
17 | },
18 | "dependencies": {
19 | "babel-runtime": "^6.5.0",
20 | "moment": "^2.11.2",
21 | "react": "^0.14.2",
22 | "react-dom": "^0.14.2",
23 | "react-redux": "^4.0.0",
24 | "redux": "^3.0.4",
25 | "redux-saga-rxjs": "^0.2.0",
26 | "rxjs": "^5.0.0-beta.2"
27 | },
28 | "author": "Tomas Weiss ",
29 | "license": "MIT"
30 | }
31 |
--------------------------------------------------------------------------------
/examples/undo-redo-optimistic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-saga-rxjs-example-undo-redo-optimistic",
3 | "version": "0.1.0",
4 | "scripts": {
5 | "start": "./node_modules/.bin/webpack-dev-server --config webpack.config.js --port 3000 --hot --content-base ./dev"
6 | },
7 | "devDependencies": {
8 | "babel-cli": "^6.5.1",
9 | "babel-core": "^6.5.2",
10 | "babel-eslint": "^4.1.8",
11 | "babel-loader": "^6.2.2",
12 | "babel-preset-es2015": "^6.5.0",
13 | "babel-preset-react": "^6.5.0",
14 | "babel-preset-stage-2": "^6.5.0",
15 | "webpack": "^1.12.4",
16 | "webpack-dev-server": "^1.12.1"
17 | },
18 | "dependencies": {
19 | "babel-runtime": "^6.5.0",
20 | "react": "^0.14.2",
21 | "react-dom": "^0.14.2",
22 | "react-redux": "^4.0.0",
23 | "redux": "^3.0.4",
24 | "redux-saga-rxjs": "^0.2.0",
25 | "rxjs": "^5.0.0-beta.2"
26 | },
27 | "author": "Tomas Weiss ",
28 | "license": "MIT"
29 | }
30 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { Subject } from 'rxjs';
2 |
3 | const isFunction = any => typeof any === 'function';
4 |
5 | const invariant = (condition, message) => {
6 | if (condition) {
7 | throw new Error(`Invariant violation: ${message}`);
8 | }
9 | };
10 |
11 | export default (...sagas) => {
12 | const subject = new Subject();
13 |
14 | invariant(sagas.length === 0,
15 | 'Provide at least one saga as argument');
16 |
17 | invariant(!sagas.every(isFunction),
18 | 'All the provided sagas must be typeof function');
19 |
20 | return store => {
21 | sagas.forEach(saga => {
22 | const iterable = saga(subject);
23 |
24 | invariant(iterable === subject,
25 | 'It is not allowed to provide identity (empty) saga');
26 |
27 | iterable.subscribe(dispatchable => store.dispatch(dispatchable));
28 | });
29 |
30 | return next => action => {
31 | const result = next(action);
32 | subject.next({action, state: store.getState()});
33 | return result;
34 | };
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/examples/undo-redo-optimistic/src/actions/actionCreators.js:
--------------------------------------------------------------------------------
1 | import * as Actions from '../constants/actions';
2 |
3 | export const changeText = text => ({ type: Actions.CHANGE_TEXT, payload: text });
4 | export const addTodo = title => ({ type: Actions.ADD_TODO, payload: title });
5 | export const undo = command => ({ type: Actions.UNDO, payload: command });
6 | export const redo = command => ({ type: Actions.REDO, payload: command });
7 | export const addTodoId = (clientId, title) => ({ type: Actions.ADD_TODO_ID, payload: { clientId, title }});
8 | export const clearRedoLog = () => ({ type: Actions.CLEAR_REDO_LOG, payload: null });
9 | export const todoAdded = (serverId, clientId, command) => ({ type: Actions.TODO_ADDED, payload: { serverId, clientId, command }});
10 | export const addTodoFailed = clientId => ({ type: Actions.ADD_TODO_FAILED, payload: clientId });
11 | export const undone = action => ({ type: Actions.UNDONE, payload: action });
12 | export const undoFailed = serverId => ({ type: Actions.UNDO_FAILED, payload: serverId });
13 | export const redone = () => ({ type: Actions.REDONE, payload: null });
14 |
--------------------------------------------------------------------------------
/examples/auth/src/reducers/rootReducer.js:
--------------------------------------------------------------------------------
1 | import * as Actions from '../constants/actions';
2 |
3 | const initialAppState = {
4 | loggedIn: false,
5 | lastTokenRefresh: '',
6 | apiInProgress: false,
7 | credentials: 'saga',
8 | loginError: false
9 | };
10 |
11 | export default (appState = initialAppState, { type, payload }) => {
12 |
13 | switch (type) {
14 | case Actions.CHANGE_CREDENTIALS:
15 | return { ...appState, credentials: payload };
16 |
17 | case Actions.LOG_IN:
18 | return { ...appState, apiInProgress: true };
19 |
20 | case Actions.LOG_OUT:
21 | return initialAppState;
22 |
23 | case Actions.LOGGED_IN:
24 | return { ...appState, loggedIn: true, apiInProgress: false, lastTokenRefresh: payload.refreshed };
25 |
26 | case Actions.LOG_IN_FAILURE:
27 | return { ...appState, loggedIn: false, apiInProgress: false, loginError: true };
28 |
29 | case Actions.TOKEN_REFRESHED:
30 | return { ...appState, lastTokenRefresh: payload };
31 |
32 | case Actions.HIDE_TOAST:
33 | return { ...appState, loginError: false };
34 |
35 | default:
36 | return appState;
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/examples/auth/src/components/Application.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import * as ActionCreators from '../actions/actionCreators';
5 |
6 | export default connect(appState => appState)(({ dispatch, credentials, loginError, loggedIn, lastTokenRefresh, apiInProgress }) => {
7 | if (apiInProgress) {
8 | return API call in progress
;
9 | } else if (loggedIn) {
10 | return (
11 |
12 |
Token last refreshed {lastTokenRefresh}
13 |
14 |
15 | );
16 | } else {
17 | return (
18 |
19 |
20 | dispatch(ActionCreators.changeCredentials(ev.target.value))}
25 | />
26 |
27 | {loginError ? Invalid credentials provided : false}
28 |
29 | );
30 | }
31 | });
32 |
--------------------------------------------------------------------------------
/examples/real-world/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-saga-rxjs-example-real-world",
3 | "version": "0.1.0",
4 | "scripts": {
5 | "start": "./node_modules/.bin/webpack-dev-server --config webpack.config.js --port 3000 --hot --content-base ./dev"
6 | },
7 | "devDependencies": {
8 | "babel-cli": "^6.5.1",
9 | "babel-core": "^6.5.2",
10 | "babel-eslint": "^4.1.8",
11 | "babel-loader": "^6.2.2",
12 | "babel-preset-es2015": "^6.5.0",
13 | "babel-preset-react": "^6.5.0",
14 | "babel-preset-stage-2": "^6.5.0",
15 | "redux-devtools": "^3.1.1",
16 | "redux-devtools-dock-monitor": "^1.0.1",
17 | "redux-devtools-log-monitor": "^1.0.4",
18 | "webpack": "^1.12.4",
19 | "webpack-dev-server": "^1.12.1"
20 | },
21 | "dependencies": {
22 | "babel-runtime": "^6.5.0",
23 | "humps": "^1.0.0",
24 | "isomorphic-fetch": "^2.2.1",
25 | "lodash": "^4.0.0",
26 | "normalizr": "^2.0.0",
27 | "react": "^0.14.2",
28 | "react-dom": "^0.14.2",
29 | "react-redux": "^4.0.0",
30 | "react-router": "^2.0.0",
31 | "react-router-redux": "^4.0.0-rc.1",
32 | "redux": "^3.0.4",
33 | "redux-logger": "^2.5.2",
34 | "redux-saga-rxjs": "^0.2.0",
35 | "rxjs": "^5.0.0-beta.2"
36 | },
37 | "author": "Tomas Weiss ",
38 | "license": "MIT"
39 | }
40 |
--------------------------------------------------------------------------------
/examples/real-world/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export const USER_REQUEST = 'USER_REQUEST';
2 | export const USER_SUCCESS = 'USER_SUCCESS';
3 | export const USER_FAILURE = 'USER_FAILURE';
4 |
5 | export function loadUser(login, requiredFields = []) {
6 | return {
7 | type: USER_REQUEST,
8 | login,
9 | requiredFields
10 | };
11 | }
12 |
13 | export const REPO_REQUEST = 'REPO_REQUEST';
14 | export const REPO_SUCCESS = 'REPO_SUCCESS';
15 | export const REPO_FAILURE = 'REPO_FAILURE';
16 |
17 | export function loadRepo(fullName, requiredFields = []) {
18 | return {
19 | type: REPO_REQUEST,
20 | fullName,
21 | requiredFields
22 | };
23 | }
24 |
25 | export const STARRED_REQUEST = 'STARRED_REQUEST';
26 | export const STARRED_SUCCESS = 'STARRED_SUCCESS';
27 | export const STARRED_FAILURE = 'STARRED_FAILURE';
28 |
29 | export function loadStarred(login, nextPage) {
30 | return {
31 | type: STARRED_REQUEST,
32 | login,
33 | nextPage
34 | };
35 | }
36 |
37 | export const STARGAZERS_REQUEST = 'STARGAZERS_REQUEST';
38 | export const STARGAZERS_SUCCESS = 'STARGAZERS_SUCCESS';
39 | export const STARGAZERS_FAILURE = 'STARGAZERS_FAILURE';
40 |
41 | export function loadStargazers(fullName, nextPage) {
42 | return {
43 | type: STARGAZERS_REQUEST,
44 | fullName,
45 | nextPage
46 | };
47 | }
48 |
49 | export const RESET_ERROR_MESSAGE = 'RESET_ERROR_MESSAGE';
50 |
51 | // Resets the currently visible error message.
52 | export function resetErrorMessage() {
53 | return {
54 | type: RESET_ERROR_MESSAGE
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/examples/undo-redo-optimistic/src/components/Application.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import * as ActionCreators from '../actions/actionCreators';
5 |
6 | const renderUndoButton = (dispatch, undo, apiInProgress) => {
7 | if (undo.length > 0) {
8 | const lastUndo = undo[undo.length - 1];
9 |
10 | return ;
11 | } else {
12 | return false;
13 | }
14 | };
15 |
16 | const renderRedoButton = (dispatch, redo, apiInProgress) => {
17 | if (redo.length > 0) {
18 | const firstRedo = redo[0];
19 |
20 | return ;
21 | } else {
22 | return false;
23 | }
24 | };
25 |
26 | export default connect(appState => appState)(({ dispatch, apiInProgress, text, todos, commands }) => {
27 | return (
28 |
29 |
30 | {todos.map((todo, index) => (
31 | - {todo.title}
35 | ))}
36 |
37 |
dispatch(ActionCreators.changeText(ev.target.value))}
41 | onKeyDown={ev => ev.keyCode === 13 ? dispatch(ActionCreators.addTodo(text)) : null} />
42 | {renderUndoButton(dispatch, commands.undo, apiInProgress)}
43 | {renderRedoButton(dispatch, commands.redo, apiInProgress)}
44 |
45 | );
46 | });
47 |
--------------------------------------------------------------------------------
/examples/real-world/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import * as ActionTypes from '../actions';
2 | import merge from 'lodash/merge';
3 | import paginate from './paginate';
4 | import { routerReducer as routing } from 'react-router-redux';
5 | import { combineReducers } from 'redux';
6 |
7 | // Updates an entity cache in response to any action with response.entities.
8 | function entities(state = { users: {}, repos: {} }, action) {
9 | if (action.response && action.response.entities) {
10 | return merge({}, state, action.response.entities);
11 | }
12 |
13 | return state;
14 | }
15 |
16 | // Updates error message to notify about the failed fetches.
17 | function errorMessage(state = null, action) {
18 | const { type, error } = action;
19 |
20 | if (type === ActionTypes.RESET_ERROR_MESSAGE) {
21 | return null;
22 | } else if (error) {
23 | return action.error;
24 | }
25 |
26 | return state;
27 | }
28 |
29 | // Updates the pagination data for different actions.
30 | const pagination = combineReducers({
31 | starredByUser: paginate({
32 | mapActionToKey: action => action.login,
33 | types: [
34 | ActionTypes.STARRED_REQUEST,
35 | ActionTypes.STARRED_SUCCESS,
36 | ActionTypes.STARRED_FAILURE
37 | ]
38 | }),
39 | stargazersByRepo: paginate({
40 | mapActionToKey: action => action.fullName,
41 | types: [
42 | ActionTypes.STARGAZERS_REQUEST,
43 | ActionTypes.STARGAZERS_SUCCESS,
44 | ActionTypes.STARGAZERS_FAILURE
45 | ]
46 | })
47 | });
48 |
49 | const rootReducer = combineReducers({
50 | entities,
51 | pagination,
52 | errorMessage,
53 | routing
54 | });
55 |
56 | export default rootReducer;
57 |
--------------------------------------------------------------------------------
/examples/real-world/src/components/Explore.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | const GITHUB_REPO = 'https://github.com/rackt/redux';
4 |
5 | export default class Explore extends Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | this.handleKeyUp = this.handleKeyUp.bind(this);
10 | this.handleGoClick = this.handleGoClick.bind(this);
11 | }
12 |
13 | componentWillReceiveProps(nextProps) {
14 | if (nextProps.value !== this.props.value) {
15 | this.setInputValue(nextProps.value);
16 | }
17 | }
18 |
19 | getInputValue() {
20 | return this.refs.input.value;
21 | }
22 |
23 | setInputValue(val) {
24 | // Generally mutating DOM is a bad idea in React components,
25 | // but doing this for a single uncontrolled field is less fuss
26 | // than making it controlled and maintaining a state for it.
27 | this.refs.input.value = val;
28 | }
29 |
30 | handleKeyUp(e) {
31 | if (e.keyCode === 13) {
32 | this.handleGoClick();
33 | }
34 | }
35 |
36 | handleGoClick() {
37 | this.props.onChange(this.getInputValue());
38 | }
39 |
40 | render() {
41 | return (
42 |
43 |
Type a username or repo full name and hit 'Go':
44 |
48 |
51 |
52 | Code on Github.
53 |
54 |
55 | Move the DevTools with Ctrl+W or hide them with Ctrl+H.
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | Explore.propTypes = {
63 | value: PropTypes.string.isRequired,
64 | onChange: PropTypes.func.isRequired
65 | };
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-saga-rxjs",
3 | "version": "0.3.0",
4 | "description": "Saga pattern for Redux implemented using rxjs",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib",
8 | "src"
9 | ],
10 | "jsnext:main": "src/index.js",
11 | "scripts": {
12 | "build:lib": "./node_modules/.bin/babel src --out-dir lib",
13 | "check": "npm run lint && npm run test",
14 | "lint": "./node_modules/.bin/eslint src/",
15 | "preversion": "npm run check",
16 | "version": "npm run build:lib",
17 | "postversion": "git push && git push --tags",
18 | "prepublish": "npm run build:lib",
19 | "test": "./node_modules/.bin/mocha --require babel-core/register --recursive",
20 | "test:cov": "./node_modules/.bin/babel-node $(npm bin)/isparta cover $(npm bin)/_mocha -- --recursive",
21 | "test:watch": "npm test -- --watch"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/tomkis1/redux-saga-rxjs.git"
26 | },
27 | "keywords": [
28 | "redux",
29 | "saga",
30 | "rxjs"
31 | ],
32 | "author": "Tomas Weiss ",
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/tomkis1/redux-saga-rxjs/issues"
36 | },
37 | "engines": {
38 | "node": ">=5.0.0",
39 | "npm": ">=3.0.0"
40 | },
41 | "homepage": "https://github.com/tomkis1/redux-saga-rxjs#readme",
42 | "peerDependencies": {
43 | "rxjs": "^5.0.0-beta.2"
44 | },
45 | "devDependencies": {
46 | "babel-cli": "^6.5.1",
47 | "babel-core": "^6.5.2",
48 | "babel-eslint": "^5.0.0",
49 | "babel-preset-es2015": "^6.5.0",
50 | "babel-preset-stage-2": "^6.5.0",
51 | "chai": "^3.5.0",
52 | "eslint": "^2.1.0",
53 | "estraverse-fb": "^1.3.1",
54 | "isparta": "^4.0.0",
55 | "mocha": "^2.4.5",
56 | "redux": "^3.3.1",
57 | "rxjs": "^5.0.0-beta.2",
58 | "sinon": "^1.17.3"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/examples/real-world/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { hashHistory } from 'react-router';
4 | import Explore from '../components/Explore';
5 | import { resetErrorMessage } from '../actions';
6 |
7 | class App extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.handleChange = this.handleChange.bind(this);
11 | this.handleDismissClick = this.handleDismissClick.bind(this);
12 | }
13 |
14 | handleDismissClick(e) {
15 | this.props.resetErrorMessage();
16 | e.preventDefault();
17 | }
18 |
19 | handleChange(nextValue) {
20 | hashHistory.push(`/${nextValue}`);
21 | }
22 |
23 | renderErrorMessage() {
24 | const { errorMessage } = this.props;
25 | if (!errorMessage) {
26 | return null;
27 | }
28 |
29 | return (
30 |
31 | {errorMessage}
32 | {' '}
33 | (
35 | Dismiss
36 | )
37 |
38 | );
39 | }
40 |
41 | render() {
42 | const { children, inputValue } = this.props;
43 | return (
44 |
45 |
47 |
48 | {this.renderErrorMessage()}
49 | {children}
50 |
51 | );
52 | }
53 | }
54 |
55 | App.propTypes = {
56 | // Injected by React Redux
57 | errorMessage: PropTypes.string,
58 | resetErrorMessage: PropTypes.func.isRequired,
59 | inputValue: PropTypes.string.isRequired,
60 | // Injected by React Router
61 | children: PropTypes.node
62 | };
63 |
64 | function mapStateToProps(state, ownProps) {
65 | return {
66 | errorMessage: state.errorMessage,
67 | inputValue: ownProps.location.pathname.substring(1)
68 | };
69 | }
70 |
71 | export default connect(mapStateToProps, {
72 | resetErrorMessage
73 | })(App);
74 |
--------------------------------------------------------------------------------
/examples/real-world/src/reducers/paginate.js:
--------------------------------------------------------------------------------
1 | import merge from 'lodash/merge';
2 | import union from 'lodash/union';
3 |
4 | // Creates a reducer managing pagination, given the action types to handle,
5 | // and a function telling how to extract the key from an action.
6 | export default function paginate({ types, mapActionToKey }) {
7 | if (!Array.isArray(types) || types.length !== 3) {
8 | throw new Error('Expected types to be an array of three elements.');
9 | }
10 | if (!types.every(t => typeof t === 'string')) {
11 | throw new Error('Expected types to be strings.');
12 | }
13 | if (typeof mapActionToKey !== 'function') {
14 | throw new Error('Expected mapActionToKey to be a function.');
15 | }
16 |
17 | const [ requestType, successType, failureType ] = types;
18 |
19 | function updatePagination(state = {
20 | isFetching: false,
21 | nextPageUrl: undefined,
22 | pageCount: 0,
23 | ids: []
24 | }, action) {
25 | switch (action.type) {
26 | case requestType:
27 | return merge({}, state, {
28 | isFetching: true
29 | });
30 | case successType:
31 | return merge({}, state, {
32 | isFetching: false,
33 | ids: union(state.ids, action.response.result),
34 | nextPageUrl: action.response.nextPageUrl,
35 | pageCount: state.pageCount + 1
36 | });
37 | case failureType:
38 | return merge({}, state, {
39 | isFetching: false
40 | });
41 | default:
42 | return state;
43 | }
44 | }
45 |
46 | return function updatePaginationByKey(state = {}, action) {
47 | switch (action.type) {
48 | case requestType:
49 | case successType:
50 | case failureType:
51 | const key = mapActionToKey(action);
52 | if (typeof key !== 'string') {
53 | throw new Error('Expected key to be a string.');
54 | }
55 | return merge({}, state, {
56 | [key]: updatePagination(state[key], action)
57 | });
58 | default:
59 | return state;
60 | }
61 | };
62 | }
63 |
--------------------------------------------------------------------------------
/examples/real-world/src/sagas/api.js:
--------------------------------------------------------------------------------
1 | import { Schema, arrayOf, normalize } from 'normalizr';
2 | import { camelizeKeys } from 'humps';
3 | import 'isomorphic-fetch';
4 |
5 | // Extracts the next page URL from Github API response.
6 | function getNextPageUrl(response) {
7 | const link = response.headers.get('Link');
8 | if (!link) {
9 | return null;
10 | }
11 |
12 | const nextLink = link.split(',').find(s => s.indexOf('rel="next"') > -1);
13 | if (!nextLink) {
14 | return null;
15 | }
16 |
17 | return nextLink.split(';')[0].slice(1, -1);
18 | }
19 |
20 | const API_ROOT = 'https://api.github.com/';
21 |
22 | // Fetches an API response and normalizes the result JSON according to schema.
23 | // This makes every API response have the same shape, regardless of how nested it was.
24 | export function callApi(endpoint, schema) {
25 | const fullUrl = (endpoint.indexOf(API_ROOT) === -1) ? API_ROOT + endpoint : endpoint;
26 |
27 | return fetch(fullUrl)
28 | .then(response =>
29 | response.json().then(json => ({ json, response }))
30 | ).then(({ json, response }) => {
31 | if (!response.ok) {
32 | return Promise.reject(json);
33 | }
34 |
35 | const camelizedJson = camelizeKeys(json);
36 | const nextPageUrl = getNextPageUrl(response);
37 |
38 | return Object.assign({},
39 | normalize(camelizedJson, schema),
40 | { nextPageUrl }
41 | );
42 | });
43 | }
44 |
45 | // We use this Normalizr schemas to transform API responses from a nested form
46 | // to a flat form where repos and users are placed in `entities`, and nested
47 | // JSON objects are replaced with their IDs. This is very convenient for
48 | // consumption by reducers, because we can easily build a normalized tree
49 | // and keep it updated as we fetch more data.
50 |
51 | // Read more about Normalizr: https://github.com/gaearon/normalizr
52 |
53 | const userSchema = new Schema('users', {
54 | idAttribute: 'login'
55 | });
56 |
57 | const repoSchema = new Schema('repos', {
58 | idAttribute: 'fullName'
59 | });
60 |
61 | repoSchema.define({
62 | owner: userSchema
63 | });
64 |
65 | // Schemas for Github API responses.
66 | export const Schemas = {
67 | USER: userSchema,
68 | USER_ARRAY: arrayOf(userSchema),
69 | REPO: repoSchema,
70 | REPO_ARRAY: arrayOf(repoSchema)
71 | };
72 |
--------------------------------------------------------------------------------
/test/sagaMiddleware.test.js:
--------------------------------------------------------------------------------
1 | import { spy } from 'sinon';
2 | import { assert } from 'chai';
3 |
4 | import sagaMiddleware from '../src/index';
5 | import { createStore, applyMiddleware } from 'redux';
6 |
7 | describe('SagaMiddleware test', () => {
8 | it('should allow mounting multiple sagas which may potentially interact in action chain', () => {
9 | const sagaA = iterable => iterable
10 | .filter(({ action }) => action.type === 'FOO')
11 | .map(() => ({type: 'BAR'}));
12 |
13 | const sagaB = iterable => iterable
14 | .filter(({ action }) => action.type === 'BAR')
15 | .map(() => ({type: 'BAZ'}));
16 |
17 | const reducer = spy((appState = 0) => appState);
18 | const store = applyMiddleware(sagaMiddleware(sagaA, sagaB))(createStore)(reducer);
19 | store.dispatch({type: 'FOO'});
20 |
21 | assert.isTrue(reducer.getCall(1).calledWith(0, {type: 'FOO'}));
22 | assert.isTrue(reducer.getCall(2).calledWith(0, {type: 'BAR'}));
23 | assert.isTrue(reducer.getCall(3).calledWith(0, {type: 'BAZ'}));
24 | });
25 |
26 | it('should throw an invariant when non function is provided', () => {
27 | try {
28 | sagaMiddleware(() => {}, 'foobar');
29 | assert.isTrue(false);
30 | } catch (ex) {
31 | assert.equal(ex.message, 'Invariant violation: All the provided sagas must be typeof function');
32 | }
33 | });
34 |
35 | it('should throw an invariant when no argument is provided', () => {
36 | try {
37 | sagaMiddleware();
38 | assert.isTrue(false);
39 | } catch (ex) {
40 | assert.equal(ex.message, 'Invariant violation: Provide at least one saga as argument');
41 | }
42 | });
43 |
44 | it('should not allow to accept identity saga', () => {
45 | const identitySaga = iterable => iterable;
46 |
47 | try {
48 | applyMiddleware(sagaMiddleware(identitySaga))(createStore)(appState => appState);
49 | assert.isTrue(false);
50 | } catch (ex) {
51 | assert.equal(ex.message, 'Invariant violation: It is not allowed to provide identity (empty) saga');
52 | }
53 | });
54 |
55 | it('should pass the action down the middleware chain', () => {
56 | const saga = iterable => iterable
57 | .filter(({ action }) => action.type === 'FOO')
58 | .map(() => ({type: 'BAR'}));
59 |
60 | const identity = input => input;
61 | const store = { getState: identity, dispatch: identity };
62 | const action = { type: 'FOO' };
63 |
64 | const result = sagaMiddleware(saga)(store)(identity)(action)
65 |
66 | assert.equal(result, action);
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/examples/real-world/src/containers/UserPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { loadUser, loadStarred } from '../actions';
4 | import User from '../components/User';
5 | import Repo from '../components/Repo';
6 | import List from '../components/List';
7 | import zip from 'lodash/zip';
8 |
9 | function loadData(props) {
10 | const { login } = props;
11 | props.loadUser(login, [ 'name' ]);
12 | props.loadStarred(login);
13 | }
14 |
15 | class UserPage extends Component {
16 | constructor(props) {
17 | super(props);
18 | this.renderRepo = this.renderRepo.bind(this);
19 | this.handleLoadMoreClick = this.handleLoadMoreClick.bind(this);
20 | }
21 |
22 | componentWillMount() {
23 | loadData(this.props);
24 | }
25 |
26 | componentWillReceiveProps(nextProps) {
27 | if (nextProps.login !== this.props.login) {
28 | loadData(nextProps);
29 | }
30 | }
31 |
32 | handleLoadMoreClick() {
33 | this.props.loadStarred(this.props.login, true);
34 | }
35 |
36 | renderRepo([ repo, owner ]) {
37 | return (
38 |
41 | );
42 | }
43 |
44 | render() {
45 | const { user, login } = this.props;
46 | if (!user) {
47 | return Loading {login}’s profile...
;
48 | }
49 |
50 | const { starredRepos, starredRepoOwners, starredPagination } = this.props;
51 | return (
52 |
53 |
54 |
55 |
60 |
61 | );
62 | }
63 | }
64 |
65 | UserPage.propTypes = {
66 | login: PropTypes.string.isRequired,
67 | user: PropTypes.object,
68 | starredPagination: PropTypes.object,
69 | starredRepos: PropTypes.array.isRequired,
70 | starredRepoOwners: PropTypes.array.isRequired,
71 | loadUser: PropTypes.func.isRequired,
72 | loadStarred: PropTypes.func.isRequired
73 | };
74 |
75 | function mapStateToProps(state, ownProps) {
76 | const { login } = ownProps.params;
77 | const {
78 | pagination: { starredByUser },
79 | entities: { users, repos }
80 | } = state;
81 |
82 | const starredPagination = starredByUser[login] || { ids: [] };
83 | const starredRepos = starredPagination.ids.map(id => repos[id]);
84 | const starredRepoOwners = starredRepos.map(repo => users[repo.owner]);
85 |
86 | return {
87 | login,
88 | starredRepos,
89 | starredRepoOwners,
90 | starredPagination,
91 | user: users[login]
92 | };
93 | }
94 |
95 | export default connect(mapStateToProps, {
96 | loadUser,
97 | loadStarred
98 | })(UserPage);
99 |
--------------------------------------------------------------------------------
/examples/real-world/src/containers/RepoPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { loadRepo, loadStargazers } from '../actions';
4 | import Repo from '../components/Repo';
5 | import User from '../components/User';
6 | import List from '../components/List';
7 |
8 | function loadData(props) {
9 | const { fullName } = props;
10 | props.loadRepo(fullName, [ 'description' ]);
11 | props.loadStargazers(fullName);
12 | }
13 |
14 | class RepoPage extends Component {
15 | constructor(props) {
16 | super(props);
17 | this.renderUser = this.renderUser.bind(this);
18 | this.handleLoadMoreClick = this.handleLoadMoreClick.bind(this);
19 | }
20 |
21 | componentWillMount() {
22 | loadData(this.props);
23 | }
24 |
25 | componentWillReceiveProps(nextProps) {
26 | if (nextProps.fullName !== this.props.fullName) {
27 | loadData(nextProps);
28 | }
29 | }
30 |
31 | handleLoadMoreClick() {
32 | this.props.loadStargazers(this.props.fullName, true);
33 | }
34 |
35 | renderUser(user) {
36 | return (
37 |
39 | );
40 | }
41 |
42 | render() {
43 | const { repo, owner, name } = this.props;
44 | if (!repo || !owner) {
45 | return Loading {name} details...
;
46 | }
47 |
48 | const { stargazers, stargazersPagination } = this.props;
49 | return (
50 |
51 |
53 |
54 |
59 |
60 | );
61 | }
62 | }
63 |
64 | RepoPage.propTypes = {
65 | repo: PropTypes.object,
66 | fullName: PropTypes.string.isRequired,
67 | name: PropTypes.string.isRequired,
68 | owner: PropTypes.object,
69 | stargazers: PropTypes.array.isRequired,
70 | stargazersPagination: PropTypes.object,
71 | loadRepo: PropTypes.func.isRequired,
72 | loadStargazers: PropTypes.func.isRequired
73 | };
74 |
75 | function mapStateToProps(state, ownProps) {
76 | const { login, name } = ownProps.params;
77 | const {
78 | pagination: { stargazersByRepo },
79 | entities: { users, repos }
80 | } = state;
81 |
82 | const fullName = `${login}/${name}`;
83 | const stargazersPagination = stargazersByRepo[fullName] || { ids: [] };
84 | const stargazers = stargazersPagination.ids.map(id => users[id]);
85 |
86 | return {
87 | fullName,
88 | name,
89 | stargazers,
90 | stargazersPagination,
91 | repo: repos[fullName],
92 | owner: users[login]
93 | };
94 | }
95 |
96 | export default connect(mapStateToProps, {
97 | loadRepo,
98 | loadStargazers
99 | })(RepoPage);
100 |
--------------------------------------------------------------------------------
/examples/auth/src/sagas/authSaga.js:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs';
2 | import moment from 'moment';
3 |
4 | import * as Actions from '../constants/actions';
5 | import * as ActionCreators from '../actions/actionCreators';
6 |
7 | const logInApi = crendetials => new Promise((res, rej) => setTimeout(() => {
8 | if (crendetials === 'saga') {
9 | res(moment().format('HH:mm:ss'));
10 | } else {
11 | rej('Invalid credentials');
12 | }
13 | }, 500));
14 |
15 | const createDelay = time => new Promise(res => setTimeout(() => res(), time));
16 | const actionOrder = (actions, order) => actions.every(({ action }, index) => action.type === order[index]);
17 | const actionPredicate = actions => ({ action }) => actions.some(someAction => someAction === action.type);
18 |
19 | const AUTH_EXPIRATION = 1000;
20 |
21 | const LOG_OUT_ACTIONS_ORDER = [
22 | Actions.LOG_IN,
23 | Actions.LOGGED_IN,
24 | Actions.LOG_OUT
25 | ];
26 |
27 | // User clicked the log in button,
28 | // call the API and respond with either success or failure
29 | const authGetTokenSaga = iterable => iterable
30 | .filter(actionPredicate([Actions.LOG_IN]))
31 | .flatMap(({ action }) => Observable
32 | .fromPromise(logInApi(action.payload))
33 | .map(refreshed => ActionCreators.loggedIn(action.payload, refreshed))
34 | .catch(() => Observable.of(ActionCreators.logInFailure())));
35 |
36 | // After the user is successfuly logged in,
37 | // let's schedule an infinite interval stream
38 | // which can be interrupted by LOG_OUT action
39 | const authRefreshTokenSaga = iterable => iterable
40 | .filter(actionPredicate([Actions.LOGGED_IN]))
41 | .flatMap(({ action }) => Observable
42 | .interval(AUTH_EXPIRATION)
43 | .flatMap(() => Observable
44 | .fromPromise(logInApi(action.payload.credentials))
45 | .map(refreshed => ActionCreators.tokenRefreshed(refreshed))
46 | )
47 | .takeUntil(iterable.filter(actionPredicate([Actions.LOG_OUT])))
48 | );
49 |
50 | // Observe all the actions in specific order
51 | // to determine whether user wants to log out
52 | const authHandleLogOutSaga = iterable => iterable
53 | .filter(actionPredicate(LOG_OUT_ACTIONS_ORDER))
54 | .bufferCount(LOG_OUT_ACTIONS_ORDER.length)
55 | .filter(actions => actionOrder(actions, LOG_OUT_ACTIONS_ORDER))
56 | .map(() => ActionCreators.logOut());
57 |
58 | // After LOG_IN_FAILURE kicks-in, start a race
59 | // between 5000ms delay and CHANGE_CREDENTIALS action,
60 | // meaning that either timeout or changing credentials
61 | // hides the toast
62 | const authShowLogInFailureToast = iterable => iterable
63 | .filter(actionPredicate([Actions.LOG_IN_FAILURE]))
64 | .flatMap(() =>
65 | Observable.race(
66 | Observable.fromPromise(createDelay(5000)),
67 | iterable.filter(actionPredicate([Actions.CHANGE_CREDENTIALS]))
68 | )
69 | .map(() => ActionCreators.hideToast()));
70 |
71 | // Just merge all the sub-sagas into one sream
72 | export default iterable => Observable.merge(
73 | authGetTokenSaga(iterable),
74 | authRefreshTokenSaga(iterable),
75 | authHandleLogOutSaga(iterable),
76 | authShowLogInFailureToast(iterable)
77 | );
78 |
--------------------------------------------------------------------------------
/examples/undo-redo-optimistic/src/reducers/rootReducer.js:
--------------------------------------------------------------------------------
1 | import * as Actions from '../constants/actions';
2 |
3 | const initialAppState = {
4 | text: '',
5 | todos: [],
6 | apiInProgress: false,
7 | commands: {
8 | undo: [],
9 | redo: []
10 | }
11 | };
12 |
13 | export default (appState = initialAppState, { type, payload }) => {
14 |
15 | switch (type) {
16 | case Actions.CHANGE_TEXT:
17 | return { ...appState, text: payload };
18 |
19 | case Actions.ADD_TODO_ID:
20 | return {
21 | ...appState,
22 | apiInProgress: true,
23 | todos: [...appState.todos, { id: payload.clientId, title: payload.title, transient: true }]
24 | };
25 |
26 | case Actions.TODO_ADDED:
27 | return {
28 | ...appState,
29 | text: '',
30 | apiInProgress: false,
31 | todos: appState.todos.map(todo => {
32 | if (todo.id === payload.clientId) {
33 | return { ...todo, id: payload.serverId, transient: false };
34 | } else {
35 | return todo;
36 | }
37 | }),
38 | commands: {
39 | ...appState.commands,
40 | undo: [...appState.commands.undo, payload]
41 | }
42 | };
43 |
44 | case Actions.ADD_TODO_FAILED:
45 | return {
46 | ...appState,
47 | apiInProgress: false,
48 | todos: appState.todos.filter(todo => todo.id !== payload)
49 | };
50 |
51 |
52 | case Actions.REDONE:
53 | const redo = [...appState.commands.redo];
54 | redo.shift();
55 |
56 | return {
57 | ...appState,
58 | commands: {
59 | ...appState.commands,
60 | redo: redo
61 | }
62 | };
63 |
64 | case Actions.CLEAR_REDO_LOG:
65 | return {
66 | ...appState,
67 | commands: {
68 | ...appState.commands,
69 | redo: []
70 | }
71 | };
72 |
73 | case Actions.UNDO:
74 | return {
75 | ...appState,
76 | apiInProgress: true,
77 | todos: appState.todos.map(todo => {
78 | if (todo.id === payload.serverId) {
79 | return { ...todo, transient: true };
80 | } else {
81 | return todo;
82 | }
83 | })
84 | };
85 |
86 | case Actions.UNDO_FAILED:
87 | return {
88 | ...appState,
89 | apiInProgress: false,
90 | todos: appState.todos.map(todo => {
91 | if (todo.id === payload) {
92 | return { ...todo, transient: false };
93 | } else {
94 | return todo;
95 | }
96 | })
97 | };
98 |
99 | case Actions.UNDONE:
100 | const undo = [...appState.commands.undo];
101 | undo.pop();
102 |
103 | return {
104 | ...appState,
105 | apiInProgress: false,
106 | todos: appState.todos.filter(todo => todo.id !== payload.serverId),
107 | commands: {
108 | ...appState.commands,
109 | undo,
110 | redo: [payload.command, ...appState.commands.redo]
111 | }
112 | };
113 |
114 | default:
115 | return appState;
116 | }
117 | };
118 |
--------------------------------------------------------------------------------
/examples/real-world/src/sagas/realWorldSaga.js:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs';
2 | import { Schemas, callApi } from './api';
3 | import {
4 | USER_REQUEST, USER_SUCCESS, USER_FAILURE,
5 | STARRED_REQUEST, STARRED_SUCCESS, STARRED_FAILURE,
6 | REPO_REQUEST, REPO_SUCCESS, REPO_FAILURE,
7 | STARGAZERS_REQUEST, STARGAZERS_SUCCESS, STARGAZERS_FAILURE
8 | } from '../actions';
9 |
10 | const callApiSaga = (endpoint, schema, requestAction, successActionType, failureActionType) =>
11 | Observable.fromPromise(callApi(endpoint, schema))
12 | .map(response => ({
13 | ...requestAction,
14 | type: successActionType,
15 | response
16 | }))
17 | .catch(error => Observable.of({
18 | ...requestAction,
19 | type: failureActionType,
20 | error: error.message || 'Something bad happened'
21 | }));
22 |
23 | const fetchUser = (userRequestAction, login) => callApiSaga(
24 | `users/${login}`,
25 | Schemas.USER,
26 | userRequestAction,
27 | USER_SUCCESS,
28 | USER_FAILURE
29 | );
30 |
31 | const fetchStarred = (starredRequestAction, url) => callApiSaga(
32 | url,
33 | Schemas.REPO_ARRAY,
34 | starredRequestAction,
35 | STARRED_SUCCESS,
36 | STARRED_FAILURE
37 | );
38 |
39 | const fetchRepo = (repoRequestAction, fullName) => callApiSaga(
40 | `repos/${fullName}`,
41 | Schemas.REPO,
42 | repoRequestAction,
43 | REPO_SUCCESS,
44 | REPO_FAILURE
45 | );
46 |
47 | const fetchStargazers = (stargazersRequestAction, url) => callApiSaga(
48 | url,
49 | Schemas.USER_ARRAY,
50 | stargazersRequestAction,
51 | STARGAZERS_SUCCESS,
52 | STARGAZERS_FAILURE
53 | );
54 |
55 | const loadUserSaga = iterable => iterable
56 | .filter(({ action }) => action.type === USER_REQUEST)
57 | .filter(({ action, state }) => {
58 | const { login, requiredFields } = action;
59 | const user = state.entities.users[login];
60 | return !(user && requiredFields.every(key => user.hasOwnProperty(key)));
61 | })
62 | .flatMap(({ action }) => fetchUser(action, action.login));
63 |
64 | const loadRepoSaga = iterable => iterable
65 | .filter(({ action }) => action.type === REPO_REQUEST)
66 | .filter(({ action, state }) => {
67 | const { fullName, requiredFields } = action;
68 | const repo = state.entities.repos[fullName];
69 | return !(repo && requiredFields.every(key => repo.hasOwnProperty(key)));
70 | })
71 | .flatMap(({ action }) => fetchRepo(action, action.fullName));
72 |
73 | const paginationPredicate = ({ pageCount, nextPage }) => !pageCount > 0 || nextPage;
74 |
75 | const loadStarredSaga = iterable => iterable
76 | .filter(({ action }) => action.type === STARRED_REQUEST)
77 | .map(({ action, state }) => {
78 | const { login, nextPage } = action;
79 | const {
80 | pageCount = 0,
81 | nextPageUrl = `users/${login}/starred`
82 | } = state.pagination.starredByUser[login] || {};
83 |
84 | return {
85 | action,
86 | pageCount,
87 | nextPage,
88 | nextPageUrl
89 | };
90 | })
91 | .filter(paginationPredicate)
92 | .flatMap(({ action, nextPageUrl }) => fetchStarred(action, nextPageUrl));
93 |
94 | const loadStargazersSaga = iterable => iterable
95 | .filter(({ action }) => action.type === STARGAZERS_REQUEST)
96 | .map(({ action, state }) => {
97 | const { fullName, nextPage } = action;
98 | const {
99 | pageCount = 0,
100 | nextPageUrl = `repos/${fullName}/stargazers`
101 | } = state.pagination.stargazersByRepo[fullName] || {};
102 |
103 | return {
104 | action,
105 | pageCount,
106 | nextPage,
107 | nextPageUrl
108 | };
109 | })
110 | .filter(paginationPredicate)
111 | .flatMap(({ action, nextPageUrl }) => fetchStargazers(action, nextPageUrl));
112 |
113 | export default iterable => Observable.merge(
114 | loadUserSaga(iterable),
115 | loadStarredSaga(iterable),
116 | loadRepoSaga(iterable),
117 | loadStargazersSaga(iterable)
118 | );
119 |
--------------------------------------------------------------------------------
/examples/undo-redo-optimistic/src/sagas/commandSaga.js:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs';
2 |
3 | import * as Actions from '../constants/actions';
4 | import * as ActionCreators from '../actions/actionCreators';
5 |
6 | let globalClientId = 0;
7 | const API_TIME = 300;
8 |
9 | const unstableApi = info => {
10 | console.log(info);
11 |
12 | return new Promise((res, rej) =>
13 | setTimeout(() =>
14 | Math.random() > 0.3 ? res(Math.random()) : rej(), API_TIME));
15 | };
16 |
17 | const actionPredicate = actions => ({ action }) => actions.some(someAction => someAction === action.type);
18 |
19 | // Filter out all the user initiated (non empty) ADD_TODO actions
20 | // and map them to provide client generated ID (the ID is used for optimistic updates)
21 | //
22 | // Handling ADD_TODO_ID in reducer will create transient TODO with clientId assigned.
23 | // This todo may eventually get deleted if the API fails
24 | const generateClientIdSaga = iterable => iterable
25 | .filter(actionPredicate([Actions.ADD_TODO]))
26 | .filter(({ action }) => action.payload !== '')
27 | .map(({ action }) => ActionCreators.addTodoId(globalClientId++, action.payload));
28 |
29 | // The main saga responsible for calling the API.
30 | // Whenever ADD_TODO_ID action kicks in, API call is executed.
31 | //
32 | // When API succeeds we need to provide `clientId` and `serverId` to reducer so that it's possible to assign the serverId to
33 | // transient todo record which now becomes persistent (commited). Also it's important to provide original action which
34 | // will act as "Command" which can be later used for redo.
35 | //
36 | // If the API fails, we'll provide just clientId so that the Todo can be rolled back
37 | const createTodoSaga = iterable => iterable
38 | .filter(actionPredicate([Actions.ADD_TODO_ID]))
39 | .flatMap(({ action }) => {
40 | const clientId = action.payload.clientId;
41 | const title = action.payload.title;
42 |
43 | return Observable.fromPromise(unstableApi(`Create todo - ${title}`))
44 | .map(serverId => ActionCreators.todoAdded(serverId, clientId, action))
45 | .catch(() =>
46 | Observable.of(ActionCreators.addTodoFailed(clientId)));
47 | });
48 |
49 | // Whenever UNDO action kicks in
50 | // API call for undoing is executed and reducer is notified about success/failure by dispatching action
51 | const undoSaga = iterable => iterable
52 | .filter(actionPredicate([Actions.UNDO]))
53 | .flatMap(({ action }) =>
54 | Observable.fromPromise(unstableApi(`Undo todo - ${action.payload.serverId}`))
55 | .map(() => ActionCreators.undone(action.payload))
56 | .catch(() =>
57 | Observable.of(ActionCreators.undoFailed(action.payload.serverId))));
58 |
59 | // This is a bit tricky
60 | // 1) Redo saga must map REDO action to original Command (which in our case is ADD_TODO_ID action)
61 | // 2) However, we need to wait for successful redoing of the command - the second branch of the merge
62 | const redoSaga = iterable => iterable
63 | .filter(actionPredicate([Actions.REDO]))
64 | .flatMap(({ action }) => Observable.merge(
65 | Observable.of(action.payload),
66 | iterable
67 | .filter(actionPredicate([Actions.TODO_ADDED, Actions.ADD_TODO_FAILED]))
68 | .take(1)
69 | .filter(actionPredicate([Actions.TODO_ADDED]))
70 | .map(() => ActionCreators.redone())
71 | ));
72 |
73 | // Whenever we get ADD_TODO immediately followed by TODO_ADDED we
74 | // want to clear the redo log, because when user creates new todo, it doesn't
75 | // make sense to keep the previous redo log.
76 | const clearRedoLogSaga = iterable => iterable
77 | .filter(actionPredicate([Actions.ADD_TODO]))
78 | .flatMap(() => iterable
79 | .filter(actionPredicate([Actions.TODO_ADDED, Actions.ADD_TODO_FAILED]))
80 | .take(1)
81 | .filter(actionPredicate([Actions.TODO_ADDED]))
82 | .map(() => ActionCreators.clearRedoLog()));
83 |
84 | export default iterable => Observable.merge(
85 | generateClientIdSaga(iterable),
86 | createTodoSaga(iterable),
87 | undoSaga(iterable),
88 | redoSaga(iterable),
89 | clearRedoLogSaga(iterable)
90 | );
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | redux-saga-rxjs
2 | =============
3 |
4 | [![NPM version][npm-image]][npm-url]
5 | [![Dependencies][dependencies]][npm-url]
6 | [![Build status][travis-image]][travis-url]
7 | [![Downloads][downloads-image]][downloads-url]
8 |
9 |
10 | > RxJS implementation of [Saga pattern](https://www.youtube.com/watch?v=xDuwrtwYHu8) for [redux](https://github.com/reactjs/redux)
11 |
12 | # This project is no longer updated and mantained, it was intended as proof of concept, there is a real library with awesome documentation & API, please have a look at [redux-observable](https://github.com/redux-observable/redux-observable).
13 |
14 | ## Introduction
15 |
16 | ### Redux is great, long running transactions are problematic
17 | Redux gives us great power but with great power comes great responsibility. It's possible to build amazing, extensible, robust and scalable architecture, yet it's not as easy as it looks, because there are some unknowns which hasn't been fully solved and proven e.g. Local component State / Side effects / Long running transactions etc. One common problem that probably every developer will sooner or later have to face is communication with an API.
18 |
19 | Reading through the Redux docs will guide you to use [`thunk-middleware`](https://github.com/gaearon/redux-thunk). Thunk middleware allows you to dispatch `function` instead of `object`, this is cool because the `function` gets called providing `dispatch` and `getState` as arguments, therefore it allows you to call the API inside the function and dispatch an action holding payload when API response arrives - this is an asynchronous operation.
20 |
21 | ```javascript
22 |
23 | const apiAction = (dispatch, getState) => {
24 | dispatch({type: 'STARTED_LOADING_DATA'});
25 |
26 | api().then(response => dispatch({type: 'DATA_LOADED', response}));
27 | }
28 |
29 | const view = dispatch => (
30 |
31 | );
32 |
33 | ```
34 |
35 | The example above is something we could call long running transaction, in other words it's some kind of logic group which groups more than single Action. Long running transactions does not necessarily need to be Asynchronous, in this example the long running transaction is also asynchronous because those actions are not dispatched synchronously in sequence.
36 |
37 |
38 | ```javascript
39 | const synchronousLongRunningTransaction = (dispatch, getState) => {
40 | dispatch({type: 'FOO'});
41 |
42 | if (getState().foo === 1) {
43 | dispatch({type: 'BAR'});
44 | }
45 | }
46 | ```
47 |
48 | The example above is also a long running transaction yet it's not asynchronous and does not yield any side effects.
49 |
50 |
51 | ### What is Saga pattern?
52 | Saga is a pattern for orchestrating long running transactions... TODO
53 |
54 | ### Why is Saga good and probably mandatory for Redux?
55 | Redux is just predictable state container. We can say that all it does (and it does very well) is holding refrence of your application state and providing interface to mutate it (dispatching actions) in single transaction. Whenever any action (interaction with the UI) is dispatched, the reducer is called with provided application state and action to perform the mutation. The simplest model of Redux is State Machine because it defines two concepts: States and Transitions where in context of Redux - States are references to application state snapshot in specific time and Transitions are Actions.
56 |
57 | State Machine is missing one important piece and it's the transition history. State machine knows current state but it doesn't know anything about how it got to the state, it's missing **Sequence of transitions**. However, the Sequence of transitions is often very important information. Let's imagine you want to withdraw money from ATM, the first thing you need to do is enter your credit card and then enter the PIN. So the sequence of transitions could be as follows: `WAITING_FOR_CREDIT_CARD` -> `CARD_INSERTED` -> `AUTHORIZED` or `REJECTED` but we would like to allow user enter invalid PIN 3 times before rejecting.
58 |
59 | So first naive approach would be model of State machine which covers all the possible states and transitions between them:
60 |
61 | 
62 |
63 | As you can see, for simple use case the State machine is quite complex, now let's have a look at the State machine which would probably correspond with the way you'd implemented it in Redux.
64 |
65 | 
66 |
67 | You might have spotted something ugly in the diagram and it's the counter of attempts in the form of intermediate state. Yes, in traditional Redux you'd have to keep the information in your application state because State Machine does not allow you to keep track of transitions history. And that's exactly the point where Saga comes into play.
68 |
69 | **Long running transaction in context of Redux means specific sequence of dispatched actions**, without Saga there's no other way to know about the sequence but storing intermediate state in your application state.
70 |
71 | ```javascript
72 | const reducer = (appState, { type }) {
73 | switch (type) {
74 | case 'VALID_PIN_ENTERED':
75 | return { ...appState, authorized: true };
76 |
77 | case 'PIN_REJECTED':
78 | if (appState.attempt >= 2) {
79 | return { ...appState, authFailure: true };
80 | } else {
81 | return { ...appState, attempt: appState.attempt + 1 };
82 | }
83 |
84 | default:
85 | return appState;
86 | }
87 | }
88 | ```
89 |
90 | However, using Saga we don't need to store the intermediate state in the application state and reducers because all this lives in Saga. **The reducer would be just a simple projector of your Domain Events (Actions), where the projection is application state**.
91 |
92 | ```javascript
93 | const reducer = (appState, { type }) {
94 | switch (type) {
95 | case 'AUTHORIZED':
96 | return { ...appState, authorized: true };
97 |
98 | case 'REJECTED':
99 | return { ...appState, authFailure: true };
100 |
101 | default:
102 | return appState;
103 | }
104 | }
105 | ```
106 |
107 | ## Comparison
108 |
109 | ### Is thunk-middleware Saga pattern?
110 |
111 | [...TODO]
112 |
113 | ### What's the relation between Saga pattern and Side effects?
114 |
115 | So why people say that Saga is a great pattern for Side effects? Let's take an API call as example - you'll probably have one action (`API_REQUESTED`) dispatched when user clicks the button to load data, which presumably displays loading spinner and another action to process the response (`API_FINISHED`) and this is all in single, long running transaction which Saga can handle.
116 |
117 | We need to distinguish between Side effects and Asynchronous long running transaction. The former stands for some effect which is not the primary goal of the calling function (mutation of some external state / calling XHR / logging to console...) while the latter stands for asynchronous sequence of actions which is some logical group (transaction). Saga solves the latter, it's just an implementation detail that it's capable of solving side effects. We could still say that it should be forbidden to perform side effects in Sagas as it is in Reducers - just a minor implementation detail.
118 |
119 | ### Difference between redux-saga-rxjs and redux-saga
120 |
121 | [...TODO]
122 |
123 | ## Usage
124 |
125 | Install the package via `npm` - `npm install redux-saga-rxjs --save`. Package exposes single middleware to be used in your Redux application. The middleware takes Sagas as its arguments.
126 |
127 | ```javascript
128 | import { createStore, applyMiddleware } from 'redux';
129 | import sagaMiddleware from 'redux-saga-rxjs';
130 |
131 | // Example of simplest saga
132 | // Whenever action FOO kicks-in, Saga will dispatch
133 | // BAR action
134 | const saga = iterable => iterable
135 | .filter(({ action, state }) => action.type === 'FOO')
136 | .map(() => ({ type: 'BAR' }));
137 |
138 | const storeFactory = applyMiddleware(
139 | sagaMiddleware(saga, sagaFoo...) // You can provide more than one Saga here
140 | )(createStore);
141 |
142 | // Very simple identity reducer which is not doing anything
143 | const identityReducer = appState => appState;
144 |
145 | // Use the store as you are used to in traditional Redux application
146 | const store = storeFactory(identityReducer);
147 | ```
148 |
149 | ## Development
150 |
151 | ```
152 | npm install
153 | npm run test:watch
154 | ```
155 |
156 |
157 | [npm-image]: https://img.shields.io/npm/v/redux-saga-rxjs.svg?style=flat-square
158 | [npm-url]: https://npmjs.org/package/redux-saga-rxjs
159 | [travis-image]: https://img.shields.io/travis/salsita/redux-saga-rxjs.svg?style=flat-square
160 | [travis-url]: https://travis-ci.org/salsita/redux-saga-rxjs
161 | [downloads-image]: http://img.shields.io/npm/dm/redux-saga-rxjs.svg?style=flat-square
162 | [downloads-url]: https://npmjs.org/package/redux-saga-rxjs
163 | [dependencies]: https://david-dm.org/salsita/redux-saga-rxjs.svg
164 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint", // https://github.com/babel/babel-eslint
3 | "experimental": true,
4 | "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments
5 | "browser": true, // browser global variables
6 | "node": true, // Node.js global variables and Node.js-specific rules
7 | "mocha": true
8 | },
9 | "ecmaFeatures": {
10 | "arrowFunctions": true,
11 | "blockBindings": true,
12 | "classes": true,
13 | "defaultParams": true,
14 | "destructuring": true,
15 | "forOf": true,
16 | "generators": false,
17 | "modules": true,
18 | "objectLiteralComputedProperties": true,
19 | "objectLiteralDuplicateProperties": false,
20 | "objectLiteralShorthandMethods": true,
21 | "objectLiteralShorthandProperties": true,
22 | "spread": true,
23 | "superInFunctions": true,
24 | "templateStrings": true
25 | },
26 | "rules": {
27 | /**
28 | * Strict mode
29 | */
30 | // babel inserts "use strict"; for us
31 | "strict": [2, "never"], // http://eslint.org/docs/rules/strict
32 |
33 | /**
34 | * ES6
35 | */
36 | "no-var": 2, // http://eslint.org/docs/rules/no-var
37 | "prefer-const": 2, // http://eslint.org/docs/rules/prefer-const
38 |
39 | /**
40 | * Variables
41 | */
42 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow
43 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names
44 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars
45 | "vars": "local",
46 | "args": "after-used"
47 | }],
48 | "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define
49 |
50 | /**
51 | * Possible errors
52 | */
53 | "comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle
54 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign
55 | "no-console": 1, // http://eslint.org/docs/rules/no-console
56 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger
57 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert
58 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition
59 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys
60 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case
61 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty
62 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign
63 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast
64 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi
65 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign
66 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations
67 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp
68 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace
69 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls
70 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays
71 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable
72 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan
73 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var
74 |
75 | /**
76 | * Best practices
77 | */
78 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return
79 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly
80 | "default-case": 2, // http://eslint.org/docs/rules/default-case
81 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation
82 | "allowKeywords": true
83 | }],
84 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq
85 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in
86 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller
87 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null
88 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval
89 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native
90 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind
91 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough
92 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal
93 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval
94 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks
95 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func
96 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str
97 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign
98 | "no-new": 2, // http://eslint.org/docs/rules/no-new
99 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func
100 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers
101 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal
102 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape
103 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign
104 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto
105 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare
106 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign
107 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url
108 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare
109 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences
110 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal
111 | "no-with": 2, // http://eslint.org/docs/rules/no-with
112 | "radix": 2, // http://eslint.org/docs/rules/radix
113 | "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top
114 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife
115 | "yoda": 2, // http://eslint.org/docs/rules/yoda
116 |
117 | /**
118 | * Style
119 | */
120 | "indent": [2, 2], // http://eslint.org/docs/rules/indent
121 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style
122 | "1tbs", {
123 | "allowSingleLine": true
124 | }],
125 | "quotes": [
126 | 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes
127 | ],
128 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase
129 | "properties": "never"
130 | }],
131 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing
132 | "before": false,
133 | "after": true
134 | }],
135 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style
136 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last
137 | "func-names": 1, // http://eslint.org/docs/rules/func-names
138 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing
139 | "beforeColon": false,
140 | "afterColon": true
141 | }],
142 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap
143 | "newIsCap": true
144 | }],
145 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines
146 | "max": 2
147 | }],
148 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary
149 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object
150 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func
151 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces
152 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle
153 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var
154 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks
155 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi
156 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing
157 | "before": false,
158 | "after": true
159 | }],
160 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks
161 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren
162 | "space-infix-ops": 2 // http://eslint.org/docs/rules/space-infix-ops
163 | }
164 | }
--------------------------------------------------------------------------------