├── .babelrc
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE.md
├── README.md
├── example
├── .babelrc
├── README.md
├── index.html
├── package.json
├── src
│ ├── GifViewer.js
│ ├── main.js
│ └── reducer.js
├── test
│ └── reducer.test.js
└── webpack.config.js
├── package.json
├── src
├── combineReducers.js
├── createEffectCapableStore.js
├── enhanceReducer.js
├── index.js
├── sideEffect.js
└── utils.js
└── test
├── combineReducers.test.js
├── createEffectCapableStore.test.js
├── enhanceReducer.test.js
├── sideEffect.test.js
└── utils.test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2"],
3 | "plugins": ["transform-runtime"]
4 | }
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | node_modules
4 | lib
5 | coverage
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "5"
4 | script:
5 | - npm run lint
6 | - npm test
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Salsita software
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | redux-side-effects
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 | > What if your reducers were generators? You could yield side effects and return application state.
11 |
12 | Believe it or not, but side effects are still tied with your application's domain. Ideally, you would be able to keep them in reducers. But wait! Everybody is saying that reducers must be pure! So yeah, just keep the reducer pure and reduce side effects as well.
13 |
14 | ## Why?
15 |
16 | Some people (I am one of them) believe that [Elm](https://github.com/evancz/elm-architecture-tutorial/#example-5-random-gif-viewer) has found the proper way how to handle side effects. Yes, we have a solution for async code in [redux](https://github.com/rackt/redux) and it's [`redux-thunk`](https://github.com/gaearon/redux-thunk) but the solution has two major drawbacks:
17 |
18 | 1) Application logic is not in one place, which leads to the state where business domain may be encapsulated by service domain.
19 |
20 | 2) Unit testing of some use cases which heavy relies on side effect is nearly impossible. Yes, you can always test those things in isolation but then you will lose the context. It's breaking the logic apart, which is making it basically impossible to test as single unit.
21 |
22 | Therefore ideal solution is to keep the domain logic where it belongs (reducers) and abstract away execution of side effects. Which means that your reducers will still be pure (Yes! Also hot-reloadable and easily testable). There are basically two options, either we can abuse reducer's reduction (which is basically a form of I/O Monad) or we can simply put a bit more syntactic sugar on it.
23 |
24 | Because ES6 [generators](https://developer.mozilla.org/cs/docs/Web/JavaScript/Reference/Statements/function*) is an excellent way how to perform lazy evaluation, it's also a perfect tool for the syntax sugar to simplify working with side effects.
25 |
26 | Just imagine, you can `yield` a side effect and framework runtime is responsible for executing it after `reducer` reduces new application state. This ensures that Reducer remains pure.
27 |
28 | ```javascript
29 | import { sideEffect } from 'redux-side-effects';
30 |
31 | const loggingEffect = (dispatch, message) => console.log(message);
32 |
33 | function* reducer(appState = 1, action) {
34 | yield sideEffect(loggingEffect, 'This is side effect');
35 |
36 | return appState + 1;
37 | }
38 | ```
39 |
40 | The function is technically pure because it does not execute any side effects and given the same arguments the result is still the same.
41 |
42 | ## Usage
43 |
44 | API of this library is fairly simple, the only possible functions are `createEffectCapableStore` and `sideEffect`. `createEffectCapableStore` is a store enhancer which enables us to use Reducers in form of Generators. `sideEffect` returns declarative Side effect and allows us easy testing. In order to use it in your application you need to import it, keep in mind that it's [named import](http://www.2ality.com/2014/09/es6-modules-final.html#named_exports_%28several_per_module%29) therefore following construct is correct:
45 |
46 | `import { createEffectCapableStore } from 'redux-side-effects'`
47 |
48 | The function is responsible for creating Redux store factory. It takes just one argument which is original Redux [`createStore`](http://redux.js.org/docs/api/createStore.html) function. Of course you can provide your own enhanced implementation of `createStore` factory.
49 |
50 | To create the store it's possible to do the following:
51 |
52 | ```javascript
53 | import { createStore } from 'redux';
54 | import { createEffectCapableStore } from 'redux-side-effects';
55 |
56 | const reducer = appState => appState;
57 |
58 | const storeFactory = createEffectCapableStore(createStore);
59 | const store = storeFactory(reducer);
60 |
61 | ```
62 |
63 | Basically something like this should be fully functional:
64 |
65 | ```javascript
66 | import React from 'react';
67 | import { render } from 'react-dom';
68 | import { createStore } from 'redux';
69 | import { createEffectCapableStore, sideEffect } from 'redux-side-effects';
70 |
71 | import * as API from './API';
72 |
73 | const storeFactory = createEffectCapableStore(createStore);
74 |
75 | const addTodoEffect = (dispatch, todo) => API.addTodo(todo).then(() => dispatch({type: 'TODO_ADDED'}));
76 |
77 | const store = storeFactory(function*(appState = {todos: [], loading: false}, action) {
78 | if (action.type === 'ADD_TODO') {
79 | yield sideEffect(addTodoEffect, action.payload);
80 |
81 | return {...appState, todos: [...appState.todos, action.payload], loading: true};
82 | } else if (action.type === 'TODO_ADDED') {
83 | return {...appState, loading: false};
84 | } else {
85 | return appState;
86 | }
87 | });
88 |
89 | render(, document.getElementById('app-container'));
90 |
91 | ```
92 |
93 | ## Declarative Side Effects
94 |
95 | The `sideEffect` function is just a very simple declarative way how to express any Side Effect. Basically you can only `yield` result of the function and the function must be called with at least one argument which is Side Effect execution implementation function, all the additional arguments will be passed as arguments to your Side Effect execution implementation function.
96 |
97 | ```javascript
98 | const effectImplementation = (dispatch, arg1, arg2, arg3) => {
99 | // Your Side Effect implementation
100 | };
101 |
102 |
103 | yield sideEffect(effectImplementation, 'arg1', 'arg2', 'arg3'....);
104 | ```
105 |
106 | Be aware that first argument provided to Side Effect implementation function is always `dispatch` so that you can `dispatch` new actions within Side Effect.
107 |
108 | ## Unit testing
109 |
110 | Unit Testing with `redux-side-effects` is a breeze. You just need to assert iterable which is result of Reducer.
111 |
112 | ```javascript
113 | function* reducer(appState) {
114 | if (appState === 42) {
115 | yield sideEffect(fooEffect, 'arg1');
116 |
117 | return 1;
118 | } else {
119 | return 0;
120 | }
121 | }
122 |
123 | // Now we can effectively assert whether app state is correctly mutated and side effect is yielded.
124 | it('should yield fooEffect with arg1 when condition is met', () => {
125 | const iterable = reducer(42);
126 |
127 | assert.deepEqual(iterable.next(), {
128 | done: false,
129 | value: sideEffect(fooEffect, 'arg1')
130 | });
131 | assert.equal(iterable.next(), {
132 | done: true,
133 | value: 1
134 | });
135 | })
136 |
137 | ```
138 |
139 |
140 | ## Example
141 |
142 | There's very simple fully working example including unit tests inside `example` folder.
143 |
144 | You can check it out by:
145 | ```
146 | cd example
147 | npm install
148 | npm start
149 | open http://localhost:3000
150 | ```
151 |
152 | Or you can run tests by
153 | ```
154 | cd example
155 | npm install
156 | npm test
157 | ```
158 |
159 | ## Contribution
160 |
161 | In case you are interested in contribution, feel free to send a PR. Keep in mind that any created issue is much appreciated. For local development:
162 |
163 | ```
164 | npm install
165 | npm run test:watch
166 | ```
167 |
168 | You can also `npm link` the repo to your local Redux application so that it's possible to test the expected behaviour in real Redux application.
169 |
170 | Please for any PR, don't forget to write unit test.
171 |
172 | ## Need Help?
173 |
174 | You can reach me on [reactiflux](http://www.reactiflux.com) - username tomkis1, or DM me on [twitter](https://twitter.com/tomkisw).
175 |
176 | ## FAQ
177 |
178 | > Does redux-side-effects work with working Redux application?
179 |
180 | Yes! I set this as the major condition when started thinking about this library. Therefore the API is completely backwards compatible with any
181 | Redux application.
182 |
183 | > My middlewares are not working anymore, what has just happened?
184 |
185 | If you are using middlewares you have to use `createEffectCapableStore` for middleware enhanced store factory, not vice versa. Meaning:
186 |
187 | ```javascript
188 | const createStoreWithMiddleware = applyMiddleware(test)(createStore);
189 | const storeFactory = createEffectCapableStore(createStoreWithMiddleware);
190 | const store = storeFactory(reducer);
191 | ```
192 |
193 | is correct.
194 |
195 | > Can I compose reducers?
196 |
197 | Yes! `yield*` can help you with the composition. The concept is explained in this [gist](https://gist.github.com/tomkis1/236f6ba182b48fde4dc9)
198 |
199 | > I keep getting warning "createEffectCapableStore enhancer from redux-side-effects package is used yet the provided root reducer is not a generator function", what does that mean?
200 |
201 | Keep in mind that your root reducer needs to be generator function therefore this will throw the warning:
202 |
203 | ```javascript
204 | const storeFactory = createEffectCapableStore(createStore);
205 | const store = storeFactory(function(appState) { return appState; });
206 | ```
207 |
208 | but this will work:
209 |
210 | ```javascript
211 | const storeFactory = createEffectCapableStore(createStore);
212 | const store = storeFactory(function* (appState) { return appState; });
213 | ```
214 |
215 | > Can I use ()* => {} instead of function*()?
216 |
217 | Unfortunately no. Only `function*` is valid ES6 syntax.
218 |
219 | > I am using combineReducers, how does this work with redux-side-effects?
220 |
221 | If you are using standard Redux [`combineReducer`](http://rackt.org/redux/docs/api/combineReducers.html) in your application, please use the imported version from this package, original implementation does not work with generators. However, keep in mind that this method is [opinionated](http://rackt.org/redux/docs/api/combineReducers.html#notes) and therefore you should probably provide your own implementation.
222 |
223 | Usage is simple:
224 |
225 | `import { combineReducers } from 'redux-side-effects'`
226 |
227 |
228 | [npm-image]: https://img.shields.io/npm/v/redux-side-effects.svg?style=flat-square
229 | [npm-url]: https://npmjs.org/package/redux-side-effects
230 | [travis-image]: https://img.shields.io/travis/salsita/redux-side-effects.svg?style=flat-square
231 | [travis-url]: https://travis-ci.org/salsita/redux-side-effects
232 | [downloads-image]: http://img.shields.io/npm/dm/redux-side-effects.svg?style=flat-square
233 | [downloads-url]: https://npmjs.org/package/redux-side-effects
234 | [dependencies]: https://david-dm.org/salsita/redux-side-effects.svg
235 |
--------------------------------------------------------------------------------
/example/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2", "react"]
3 | }
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # redux-side-effects-example
2 |
3 | Run example:
4 |
5 | ```
6 | npm install
7 | npm start
8 | open http://localhost:3000
9 | ```
10 |
11 | Run tests:
12 |
13 | ```
14 | npm test
15 | ```
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |