Skip to content

Commit

Permalink
feat(epics): calling store.dispatch() directly inside your epics is…
Browse files Browse the repository at this point in the history
… now deprecated and will be removed in v1.0.0 (#336)

Instead of using `store.dispatch()` directly, your epics should emit actions you wish to dispatch through the Observable you return. Actions in, actions out. `store.dispatch()` was made available as an escape hatch, but in practice it has been a footgun for many users. It will be removed in v1.0.0
  • Loading branch information
evertbouw authored and jayphelps committed Oct 11, 2017
1 parent 434e521 commit 76ecd33
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 8 deletions.
22 changes: 15 additions & 7 deletions src/createEpicMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const defaultOptions = {
adapter: defaultAdapter
};

export function createEpicMiddleware(epic, options = defaultOptions) {
if (typeof epic !== 'function') {
export function createEpicMiddleware(rootEpic, options = defaultOptions) {
if (typeof rootEpic !== 'function') {
throw new TypeError('You must provide a root Epic to createEpicMiddleware');
}

Expand All @@ -34,9 +34,17 @@ export function createEpicMiddleware(epic, options = defaultOptions) {
return next => {
epic$
::map(epic => {
const vault = (process.env.NODE_ENV === 'production') ? store : {
getState: store.getState,
dispatch: (action) => {
console.warn(`Your Epic "${epic.name || '<anonymous>'}" called store.dispatch directly. This is an anti-pattern.`);
return store.dispatch(action);
}
};

const output$ = ('dependencies' in options)
? epic(action$, store, options.dependencies)
: epic(action$, store);
? epic(action$, vault, options.dependencies)
: epic(action$, vault);

if (!output$) {
throw new TypeError(`Your root Epic "${epic.name || '<anonymous>'}" does not return a stream. Double check you\'re not missing a return statement!`);
Expand All @@ -48,7 +56,7 @@ export function createEpicMiddleware(epic, options = defaultOptions) {
.subscribe(store.dispatch);

// Setup initial root epic
epic$.next(epic);
epic$.next(rootEpic);

return action => {
const result = next(action);
Expand All @@ -58,13 +66,13 @@ export function createEpicMiddleware(epic, options = defaultOptions) {
};
};

epicMiddleware.replaceEpic = epic => {
epicMiddleware.replaceEpic = rootEpic => {
// gives the previous root Epic a last chance
// to do some clean up
store.dispatch({ type: EPIC_END });
// switches to the new root Epic, synchronously terminating
// the previous one
epic$.next(epic);
epic$.next(rootEpic);
};

return epicMiddleware;
Expand Down
21 changes: 20 additions & 1 deletion test/createEpicMiddleware-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { of } from 'rxjs/observable/of';
import { empty } from 'rxjs/observable/empty';
import { mergeStatic } from 'rxjs/operator/merge';
import { mapTo } from 'rxjs/operator/mapTo';
import { map } from 'rxjs/operator/map';
import { ignoreElements } from 'rxjs/operator/ignoreElements';

describe('createEpicMiddleware', () => {
it('should provide epics a stream of action$ in and the "lite" store', (done) => {
Expand All @@ -20,13 +22,30 @@ describe('createEpicMiddleware', () => {
const mockMiddleware = store => next => action => {
expect(epic.calledOnce).to.equal(true);
expect(epic.firstCall.args[0]).to.be.instanceOf(ActionsObservable);
expect(epic.firstCall.args[1]).to.equal(store);
expect(epic.firstCall.args[1].getState).to.equal(store.getState);
done();
};
const store = createStore(reducer, applyMiddleware(epicMiddleware, mockMiddleware));
store.dispatch({ type: 'FIRST_ACTION_TO_TRIGGER_MIDDLEWARE' });
});

it('should warn about improper use of dispatch function', () => {
sinon.spy(console, 'warn');
const reducer = (state = [], action) => state.concat(action);
const epic = (action$, store) => action$
.ofType('PING')
::map(() => store.dispatch({ type: 'PONG' }))
::ignoreElements();

const middleware = createEpicMiddleware(epic);
const store = createStore(reducer, applyMiddleware(middleware));

store.dispatch({ type: 'PING' });

expect(console.warn.callCount).to.equal(1);
console.warn.restore();
});

it('should accept an epic that wires up action$ input to action$ out', () => {
const reducer = (state = [], action) => state.concat(action);
const epic = (action$, store) =>
Expand Down

0 comments on commit 76ecd33

Please sign in to comment.