Permalink
Browse files

refactor(thunkservables): Removed support for thunkservables

BREAKING CHANGE: Support for thunkservables has been removed, replaced by Epics. You may now use redux-thunk in tandem with redux-observable.  [Read more](http://redux-observable.js.org/docs/FAQ.html#why-were-thunkservables-removed)
  • Loading branch information...
jayphelps committed Sep 11, 2016
1 parent 82d05ea commit e55428f699eb95d0803e1ce79d89f68b7b369975
Showing with 38 additions and 225 deletions.
  1. +5 −2 docs/FAQ.md
  2. +0 −1 package.json
  3. +16 −17 src/createEpicMiddleware.js
  4. +1 −17 test/ActionsObservable-spec.js
  5. +16 −188 test/createEpicMiddleware-spec.js
@@ -26,6 +26,9 @@ Not out of box, but we provide an adapter [redux-observable-adapter-rxjs-v4](htt
It should be possible for you to create an adapter to add support. See [redux-observable-adapter-rxjs-v4](https://github.com/redux-observable/redux-observable-adapter-rxjs-v4) for reference.

<a id="miscellaneous-thunkservables-deprecated"></a>

This comment has been minimized.

@daemoon

daemoon Sep 20, 2017

Was this anchor not deleted on purpose? It looks kind of odd on FAQ page.

This comment has been minimized.

@rgbkrk

rgbkrk Sep 20, 2017

Member

This keeps old links resolving when using FAQ.md#miscellaneous-thunkservables-deprecated

This comment has been minimized.

@jayphelps

jayphelps Sep 20, 2017

Member

Yep. We used to link to that URL in old versions of redux-observable console warnings. It's been quite a while now though, so we could probably remove all mention of thunkservables

### Why were thunkservables deprecated?
<a id="why-were-thunkservables-removed"></a>
### Why were thunkservables removed?

In the original implementations of redux-observable [Epics](basics/Epics.md) were known as thunkservables because you would dispatch them, just like [redux-thunk](https://github.com/gaearon/redux-thunk). While this requires slightly less boilerplate, we found in practice it to be too inflexible when dealing with use-cases like auto-suggest/debouncing/cancellation and didn't encourage a consistent dataflow. Because we don't want to confuse people with two options that in many use cases are functionally equivalent, we decided to eventually remove thunkservables completely. We do not yet have an official date when support will be removed, but it likely will be before or at v1.0 release. Check out the documentation on [Epics](basics/Epics.md) on how to start using them.
In the original implementations of redux-observable [Epics](basics/Epics.md) were known as thunkservables because you would dispatch them, just like [redux-thunk](https://github.com/gaearon/redux-thunk). While this requires slightly less boilerplate, we found in practice it to be too inflexible when dealing with use-cases like auto-suggest/debouncing/cancellation and didn't encourage a consistent dataflow. Because we don't want to confuse people with two options that in many use cases are functionally equivalent, we decided to remove thunkservables completely. Check out the documentation on [Epics](basics/Epics.md) on how to start using them.

As a bonus, you can now easily use [redux-thunk](https://github.com/gaearon/redux-thunk) (or other thunk-like middleware) along side redux-observable! So if you'd like, you can use redux-thunk for most of your simple async and then defer to redux-observable for the more complex tasks; or to ease migration.
@@ -91,7 +91,6 @@
"rimraf": "^2.5.4",
"rxjs": "^5.0.0-beta.10",
"sinon": "1.17.5",
"symbol-observable": "^1.0.1",
"typescript": "^1.8.10",
"typings": "1.3.3",
"webpack": "^1.13.1",
@@ -1,6 +1,6 @@
import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { from } from 'rxjs/observable/from';
import { map } from 'rxjs/operator/map';
import { switchMap } from 'rxjs/operator/switchMap';
import { ActionsObservable } from './ActionsObservable';
import { EPIC_END } from './EPIC_END';
@@ -15,6 +15,10 @@ const defaultOptions = {
};

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

const input$ = new Subject();
const action$ = adapter.input(
new ActionsObservable(input$)
@@ -26,30 +30,25 @@ export function createEpicMiddleware(epic, { adapter = defaultAdapter } = defaul
store = _store;

return next => {
if (typeof epic === 'function') {
epic$::switchMap(epic => adapter.output(epic(action$, store)))
.subscribe(store.dispatch);
}
epic$
::map(epic => epic(action$, store))
::switchMap(action$ => adapter.output(action$))
.subscribe(store.dispatch);

return action => {
if (typeof action === 'function') {
if (typeof console !== 'undefined' && typeof console.warn !== 'undefined') {
console.warn('DEPRECATION: Using thunkservables with redux-observable is now deprecated in favor of the new "Epics" feature. See http://redux-observable.js.org/docs/FAQ.html#why-were-thunkservables-deprecated');
}

const out$ = from(action(action$, store));
return out$.subscribe(store.dispatch);
} else {
const result = next(action);
input$.next(action);
return result;
}
const result = next(action);
input$.next(action);
return result;
};
};
};

epicMiddleware.replaceEpic = epic => {
// 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);
};

@@ -1,8 +1,6 @@
/* globals describe it */
import { expect } from 'chai';
import { ActionsObservable, createEpicMiddleware } from '../';
import { createStore, applyMiddleware } from 'redux';
import { of } from 'rxjs/observable/of';
import { ActionsObservable } from '../';
import { Subject } from 'rxjs/Subject';

describe('ActionsObservable', () => {
@@ -19,20 +17,6 @@ describe('ActionsObservable', () => {
expect(output).to.deep.equal([{ type: 'FIRST', type: 'SECOND' }]);
});

it('should be the type provided to a dispatched function', () => {
let middleware = createEpicMiddleware();
let reducer = (state, action) => {
return state;
};

let store = createStore(reducer, applyMiddleware(middleware));

store.dispatch((arg1) => {
expect(arg1).to.be.an.instanceof(ActionsObservable);
return of({ type: 'WEEE' });
});
});

describe('ofType operator', () => {
it('should filter by action type', () => {
let actions = new Subject();
@@ -3,23 +3,29 @@ import 'babel-polyfill';
import { expect } from 'chai';
import sinon from 'sinon';
import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware, EPIC_END } from '../';
import $$observable from 'symbol-observable';
import { createEpicMiddleware, ActionsObservable, EPIC_END } from '../';
// We need to import the operators separately and not add them to the Observable
// prototype, otherwise we might accidentally cover-up that the source we're
// testing uses an operator that it does not import!
import { of } from 'rxjs/observable/of';
import { mergeStatic, merge } from 'rxjs/operator/merge';
import { empty } from 'rxjs/observable/empty';
import { mergeStatic } from 'rxjs/operator/merge';
import { mapTo } from 'rxjs/operator/mapTo';
import { delay } from 'rxjs/operator/delay';
import { takeUntil } from 'rxjs/operator/takeUntil';
import { take } from 'rxjs/operator/take';
import { filter } from 'rxjs/operator/filter';
import { map } from 'rxjs/operator/map';
import { startWith } from 'rxjs/operator/startWith';

describe('createEpicMiddleware', () => {
it('should accept a epic argument that wires up a stream of actions to a stream of actions', () => {
it('should provide epics a stream of action$ in and the "lite" store', () => {
const reducer = (state = [], action) => state.concat(action);
const epic = sinon.stub().returns(empty());
const epicMiddleware = createEpicMiddleware(epic);
const mockMiddleware = store => next => action => {
expect(epic).to.have.been.calledOnce();
expect(epic.firstCall.args[0]).to.be.instanceOf(ActionsObservable);
expect(epic.firstCall.args[1]).to.equal(store);
};
createStore(reducer, applyMiddleware(epicMiddleware, mockMiddleware));
});

it('should accept an epic that wires up action$ input to action$ out', () => {
const reducer = (state = [], action) => state.concat(action);
const epic = (action$, store) =>
mergeStatic(
@@ -96,184 +102,6 @@ describe('createEpicMiddleware', () => {
]);
});

it('emit warning that thunkservable are deprecated', () => {
sinon.spy(console, 'warn');

const reducer = (state = [], action) => state.concat(action);
const middleware = createEpicMiddleware();
const store = createStore(reducer, applyMiddleware(middleware));

store.dispatch(() => of({ type: 'ASYNC_ACTION_1' }));

expect(console.warn.calledOnce).to.equal(true);
expect(
console.warn.calledWith('DEPRECATION: Using thunkservables with redux-observable is now deprecated in favor of the new "Epics" feature. See http://redux-observable.js.org/docs/FAQ.html#why-were-thunkservables-deprecated')
).to.equal(true);

console.warn.restore();
});

it('should intercept and process actions', (done) => {
const reducer = (state = [], action) => state.concat(action);

const middleware = createEpicMiddleware();

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

store.dispatch(() => of({ type: 'ASYNC_ACTION_1' })::delay(10));
store.dispatch(() => of({ type: 'ASYNC_ACTION_2' })::delay(20));

// HACKY: but should work until we use TestScheduler.
setTimeout(() => {
expect(store.getState()).to.deep.equal([
{ type: '@@redux/INIT' },
{ type: 'ASYNC_ACTION_1' },
{ type: 'ASYNC_ACTION_2' }
]);
done();
}, 100);
});

it('should work dispatched functions that return a promise', (done) => {
const reducer = (state = [], action) => state.concat(action);

const middleware = createEpicMiddleware();

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

store.dispatch(() => Promise.resolve({ type: 'ASYNC_ACTION_1' }));
store.dispatch(() => Promise.resolve({ type: 'ASYNC_ACTION_2' }));

// HACKY: but should work until we use TestScheduler.
setTimeout(() => {
expect(store.getState()).to.deep.equal([
{ type: '@@redux/INIT' },
{ type: 'ASYNC_ACTION_1' },
{ type: 'ASYNC_ACTION_2' }
]);
done();
}, 100);
});

it('should work with iterators/generators', (done) => {
const reducer = (state = [], action) => state.concat(action);

const middleware = createEpicMiddleware();

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

store.dispatch(() => (function *() {
yield { type: 'ASYNC_ACTION_1' };
yield { type: 'ASYNC_ACTION_2' };
})());

// HACKY: but should work until we use TestScheduler.
setTimeout(() => {
expect(store.getState()).to.deep.equal([
{ type: '@@redux/INIT' },
{ type: 'ASYNC_ACTION_1' },
{ type: 'ASYNC_ACTION_2' }
]);
done();
}, 100);
});

it('should work with observablesque arguments', (done) => {
const reducer = (state = [], action) => state.concat(action);

const middleware = createEpicMiddleware();

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

let finalized = false;

store.dispatch(() => ({
[$$observable]() {
return {
subscribe(observer) {
observer.next({ type: 'ASYNC_ACTION_1' });
observer.next({ type: 'ASYNC_ACTION_2' });
observer.complete();

return {
unsubscribe() {
finalized = true;
}
};
}
};
}
}));

// HACKY: but should work until we use TestScheduler.
setTimeout(() => {
expect(store.getState()).to.deep.equal([
{ type: '@@redux/INIT' },
{ type: 'ASYNC_ACTION_1' },
{ type: 'ASYNC_ACTION_2' }
]);

expect(finalized).to.equal(true);
done();
}, 100);
});

it('should emit POJO actions to the actions Subject', (done) => {
const reducer = (state = [], action) => state.concat(action);

const middleware = createEpicMiddleware();

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

store.dispatch(
(action$) => of({ type: 'ASYNC_ACTION_2' })
::delay(10)
::takeUntil(action$::filter(action => action.type === 'ASYNC_ACTION_ABORT'))
::merge(
action$
::map(action => ({ type: action.type + '_MERGED' }))
::take(1)
)
::startWith({ type: 'ASYNC_ACTION_1' })
);

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

// HACKY: but should work until we use TestScheduler.
setTimeout(() => {
expect(store.getState()).to.deep.equal([
{ type: '@@redux/INIT' },
{ type: 'ASYNC_ACTION_1' },
{ type: 'ASYNC_ACTION_ABORT' },
{ type: 'ASYNC_ACTION_ABORT_MERGED' }
]);
done();
}, 100);
});

it('should store.dispatch onNext to allow async actions to emit other async actions', (done) => {
const reducer = (state = [], action) => state.concat(action);

const middleware = createEpicMiddleware();

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

const action2 = (action$) => of({ type: 'ASYNC_ACTION_2' });
const action1 = (action$) => of({ type: 'ASYNC_ACTION_1' }, action2);

store.dispatch(action1);

// HACKY: but should work until we use TestScheduler.
setTimeout(() => {
expect(store.getState()).to.deep.equal([
{ type: '@@redux/INIT' },
{ type: 'ASYNC_ACTION_1' },
{ type: 'ASYNC_ACTION_2' }
]);
done();
}, 100);
});

it('supports an adapter for Epic input/output', () => {
const reducer = (state = [], action) => state.concat(action);
const epic = input => input + 1;

0 comments on commit e55428f

Please sign in to comment.