Do you want to request a feature or report a bug?
feature
What is the current behavior?
I really like the idea of background epics because it provides an intuitive way to handle multiple actions like debouncing with the powerful operators of Rx. However, tests for epics require mocking HTTP requests or other side effects.
To address the same problem, redux-saga
allows sagas to yield
effects as data, such as call
, executes the effects, and feed the results to sagas. This keeps sagas side-effect-free and testable without mocking.
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsbin.com or similar.
It's not a bug.
What is the expected behavior?
To employ the same approach to redux-observable
, the straight forward way would be express effects as actions. We can have a dedicated middleware to receive side effect requests, execute them, and dispatch
es results as actions so that we can throw out side effects of epics.
import { call } from 'redux-observable/effects';
function epic(action$, store) {
const fooReq$ = action$.ofType('FOO')
.map(action => call('FOO_REQ', webapi.getFoo, action.payload.id));
const foo$ = action$.ofType('FOO_REQ')
.map(foo => ({ type: 'FOO_FETCHED', payload: foo }));
return Observable.merge(
fooReq$,
foo$
);
}
The example above should work but it's tedious to have pairs of request observable and response observable. I want to write the pair as a single Observable
. A possible solution would be creating an operator that emits effect requests to a Subject
and returns an Observable
that emits effect responses. Let's call it as effect
operator. Here I assumed that effect
returns an Observable
of Observable
s in order to express effect's result as an Observable
.
import { call } from 'redux-observable/effects';
function epic(action$, store) {
const effect$ = new Subject();
const foo$ = action$.ofType('FOO')
.effect(action$, effect$, action => call(webapi.getFoo, action.payload.id))
.switch()
.map(foo => ({ type: 'FOO_FETCHED', payload: foo }));
return Observable.merge(
effect$,
foo$
);
}
I wrote quick sketches for the effect middleware and operator.
const EFFECT_REQUEST = Symbol('EFFECT_REQUEST');
const EFFECT_RESPONSE = Symbol('EFFECT_RESPONSE');
const effectMiddleware = store => next => action => {
if (typeof action[EFFECT_REQUEST] !== 'object') {
return next(action);
}
const { id, payload } = action[EFFECT_REQUEST];
const { type, args } = payload;
switch (type) {
case 'call':
try {
const returnValue = args[0].apply(null, args.slice(1));
if (isObservable(returnValue)) {
next({
[EFFECT_RESPONSE]: { id, type, payload: returnValue }
});
} else if (typeof returnValue.then === 'function') {
returnValue
.then(result => next({
[EFFECT_RESPONSE]: { id, type, payload: Observable.of(result) }
}))
.catch(error => next({
[EFFECT_RESPONSE]: { id, type, payload: Observable.throw(error) }
}));
} else {
next({
[EFFECT_RESPONSE]: { id, type, payload: Observable.of(returnValue) }
});
}
catch (error) {
next({
[EFFECT_RESPONSE]: { id, type, payload: Observable.throw(error) }
});
}
default:
// TODO: Handle other effect types.
return next(action);
}
};
function effectOperator(action$, effect$, callback) {
return Observable.create(subscriber => {
const id = uuid();
const source = this;
const responseSub = action$
.filter(a => a[EFFECT_RESPONSE] && a[EFFECT_RESPONSE].id === id)
.map(a => a[EFFECT_RESPONSE].payload)
.subscribe(subscriber);
const requestSub = source.subscribe(
value => {
try {
effect$.next({
[EFFECT_REQUEST]: { id, payload: callback(value) }
});
} catch (e) {
subscriber.error(e);
}
},
err => subscriber.error(err),
() => subscriber.complete()
);
// https://github.com/ReactiveX/rxjs/issues/1583
return new Subscription(() => {
[responseSub, requestSub].forEach(s => s.unsubscribe());
});
});
}
Observable.prototype.effect = effectOperator;
What do you think about this idea?
There are still some things to consider:
- Awkwardness of passing
action$
and effect$
- Awkwardness of explicitly creating a
Subject
- Epic composition
Which versions of redux-observable, and which browser and OS are affected by this issue? Did this work in previous versions of redux-observable?
It's not a bug.