redux-side-effects
What if your reducers were generators? You could yield side effects and return application state.
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.
Why?
Some people (I am one of them) believe that Elm has found the proper way how to handle side effects. Yes, we have a solution for async code in redux and it's redux-thunk
but the solution has two major drawbacks:
-
Application logic is not in one place, which leads to the state where business domain may be encapsulated by service domain.
-
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.
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.
Because ES6 generators 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.
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.
import { sideEffect } from 'redux-side-effects';
const loggingEffect = (dispatch, message) => console.log(message);
function* reducer(appState = 1, action) {
yield sideEffect(loggingEffect, 'This is side effect');
return appState + 1;
}
The function is technically pure because it does not execute any side effects and given the same arguments the result is still the same.
Usage
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 therefore following construct is correct:
import { createEffectCapableStore } from 'redux-side-effects'
The function is responsible for creating Redux store factory. It takes just one argument which is original Redux createStore
function. Of course you can provide your own enhanced implementation of createStore
factory.
To create the store it's possible to do the following:
import { createStore } from 'redux';
import { createEffectCapableStore } from 'redux-side-effects';
const reducer = appState => appState;
const storeFactory = createEffectCapableStore(createStore);
const store = storeFactory(reducer);
Basically something like this should be fully functional:
import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { createEffectCapableStore, sideEffect } from 'redux-side-effects';
import * as API from './API';
const storeFactory = createEffectCapableStore(createStore);
const addTodoEffect = (dispatch, todo) => API.addTodo(todo).then(() => dispatch({type: 'TODO_ADDED'}));
const store = storeFactory(function*(appState = {todos: [], loading: false}, action) {
if (action.type === 'ADD_TODO') {
yield sideEffect(addTodoEffect, action.payload);
return {...appState, todos: [...appState.todos, action.payload], loading: true};
} else if (action.type === 'TODO_ADDED') {
return {...appState, loading: false};
} else {
return appState;
}
});
render(<Application store={store} />, document.getElementById('app-container'));
Declarative Side Effects
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.
const effectImplementation = (dispatch, arg1, arg2, arg3) => {
// Your Side Effect implementation
};
yield sideEffect(effectImplementation, 'arg1', 'arg2', 'arg3'....);
Be aware that first argument provided to Side Effect implementation function is always dispatch
so that you can dispatch
new actions within Side Effect.
Unit testing
Unit Testing with redux-side-effects
is a breeze. You just need to assert iterable which is result of Reducer.
function* reducer(appState) {
if (appState === 42) {
yield sideEffect(fooEffect, 'arg1');
return 1;
} else {
return 0;
}
}
// Now we can effectively assert whether app state is correctly mutated and side effect is yielded.
it('should yield fooEffect with arg1 when condition is met', () => {
const iterable = reducer(42);
assert.deepEqual(iterable.next(), {
done: false,
value: sideEffect(fooEffect, 'arg1')
});
assert.equal(iterable.next(), {
done: true,
value: 1
});
})
Example
There's very simple fully working example including unit tests inside example
folder.
You can check it out by:
cd example
npm install
npm start
open http://localhost:3000
Or you can run tests by
cd example
npm install
npm test
Contribution
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:
npm install
npm run test:watch
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.
Please for any PR, don't forget to write unit test.
Need Help?
You can reach me on reactiflux - username tomkis1, or DM me on twitter.
FAQ
Does redux-side-effects work with working Redux application?
Yes! I set this as the major condition when started thinking about this library. Therefore the API is completely backwards compatible with any Redux application.
My middlewares are not working anymore, what has just happened?
If you are using middlewares you have to use createEffectCapableStore
for middleware enhanced store factory, not vice versa. Meaning:
const createStoreWithMiddleware = applyMiddleware(test)(createStore);
const storeFactory = createEffectCapableStore(createStoreWithMiddleware);
const store = storeFactory(reducer);
is correct.
Can I compose reducers?
Yes! yield*
can help you with the composition. The concept is explained in this gist
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?
Keep in mind that your root reducer needs to be generator function therefore this will throw the warning:
const storeFactory = createEffectCapableStore(createStore);
const store = storeFactory(function(appState) { return appState; });
but this will work:
const storeFactory = createEffectCapableStore(createStore);
const store = storeFactory(function* (appState) { return appState; });
Can I use ()* => {} instead of function*()?
Unfortunately no. Only function*
is valid ES6 syntax.
I am using combineReducers, how does this work with redux-side-effects?
If you are using standard Redux combineReducer
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 and therefore you should probably provide your own implementation.
Usage is simple:
import { combineReducers } from 'redux-side-effects'