Fetch an action then request and emit another using redux-observable and rxjs
So I have an epic that receives a SUBMIT_LOGIN action and then it has to run the generateDeviceId function that returns an action with an ID as a payload. After it has been processed by the reducer and the store has been updated, it should request a login and then allow it to be stored and finally redirect the user to our dashboard
const generateDeviceId = (deviceId) => (({type: GENERATE_DEVICE_ID, payload: deviceId}));
const resolveLogin = (response) => ({type: RESOLVE_LOGIN, payload: response});
const submitLogin = (email, password) => ({type: SUBMIT_LOGIN, payload: {email, password}});
const requestLogin = (email, password) => ({type: REQUEST_LOGIN, payload: {email, password}});
const loadAbout = () => ({type: LOAD_ABOUT});
const submitLoginEpic = (action$) =>
action$
.ofType(SUBMIT_LOGIN)
.mapTo(generateDeviceId(uuidv1()))
.flatMap(({payload}) => login(payload.email, payload.password)
.flatMap(({response}) => [resolveLogin(response.content), loadAbout()])
);
ps: the login
function is ajax
from rx-dom
, which returns a stream:
const AjaxRequest = (method, url, data) => {
const state = store.getState();
const {token, deviceId} = state.user;
return ajax({
method,
timeout: 10000,
body: data,
responseType: 'json',
url: url,
headers: {
token,
'device-id': deviceId,
'Content-Type': 'application/json'
}
});
};
const login = (email, password) => AjaxRequest('post', 'sign_in', {email, password});
ps2: the uuidv1
function only generates a random key (its lib)
I think (actually I am sure) that I am doing it wrong, but after two days I really don't know how to proceed.: /
UPDATE
After Sergey's first update, I changed my epic, but, unfortunately, for some reason rx-dom's
ajax does not work like Sergey's login$
observable. We are currently working on this.
const generateDeviceId = (deviceId) => (({type: GENERATE_DEVICE_ID, payload: deviceId}));
const resolveLogin = (response) => ({type: RESOLVE_LOGIN, payload: response});
const submitLogin = (email, password) => ({type: SUBMIT_LOGIN, payload: {email, password}});
const requestLogin = (email, password) => ({type: REQUEST_LOGIN, payload: {email, password}});
const loadAbout = () => ({type: LOAD_ABOUT});
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({payload}) =>
Observable.of(generateDeviceId(uuid()))
.concat(login(payload.email, payload.password)
.concatMap(({response}) => [resolveLogin(response.content), loadAbout()])
UPDATE 2
After Sergei's second update, I changed the code again and got a solution where I used two epics
both .concatMap
operator to synchronously
dispatch the actions and works as expected
.
const generateDeviceId = (deviceId) => (({type: GENERATE_DEVICE_ID, payload: deviceId}));
const resolveLogin = (response) => ({type: RESOLVE_LOGIN, payload: response});
const submitLogin = (email, password) => ({type: SUBMIT_LOGIN, payload: {email, password}});
const requestLogin = (email, password) => ({type: REQUEST_LOGIN, payload: {email, password}});
const loadAbout = () => ({type: LOAD_ABOUT});
const submitLoginEpic = (action$) =>
action$
.ofType(SUBMIT_LOGIN)
.concatMap(({payload}) => [
generateDeviceId(uuid()),
requestLogin(payload.email, payload.password)
]);
const requestLoginEpic = (action$) =>
action$
.ofType(REQUEST_LOGIN)
.mergeMap(({payload}) => login(payload.email, payload.password)
.concatMap(({response}) => [resolveLogin(response.content), loadAbout()])
source to share
If I understand correctly, you want your epic to perform the following sequence of actions in response to each SUBMIT_LOGIN
:
GENERATE_DEVICE_ID -- RESOLVE_LOGIN -- LOAD_ABOUT
Also, I think it GENERATE_DEVICE_ID
should be released immediately upon receipt for SUBMIT_LOGIN
now RESOLVE_LOGIN
and LOAD_ABOUT
should only be released after the stream returned has exited login()
.
If my guess is correct, you just need to run a nested observable (created behind each SUBMIT_LOGIN
) with an operator GENERATE_DEVICE_ID
and an operator startWith
does exactly that:
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({ payload }) =>
login(payload.email, payload.password)
.mergeMap(({ response }) => Rx.Observable.of(resolveLogin(response.content), loadAbout()))
.startWith(generateDeviceId(uuidv1()))
);
Update: One possible alternative would be to use the operator concat
: obs1.concat(obs2)
subscribe to obs2
only after completion obs1
.
Note that if login()
you need to call after dispatch GENERATE_DEVICE_ID
, you can wrap it in a cold observable:
const login$ = payload =>
Rx.Observable.create(observer => {
return login(payload.email, payload.password).subscribe(observer);
});
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({ payload }) =>
Rx.Observable.of(generateDeviceId(uuidv1()))
.concat(login$(payload).map(({ response }) => resolveLogin(response.content)))
.concat(Rx.Observable.of(loadAbout()))
);
This way is GENERATE_DEVICE_ID
emitted before being called login()
, i.e. sequence will be
GENERATE_DEVICE_ID -- login() -- RESOLVE_LOGIN -- LOAD_ABOUT
Update 2: The reason why it login()
doesn't work as expected is because it depends on the external state ( const state = getCurrentState()
), which is different at the times login()
when the observable is called and when the observable returned is subscribed login()
. AjaxRequest
captures the state at the point when called login()
, which happens before being sent GENERATE_DEVICE_ID
to the store. At this point, the network request has not yet been completed, but the ajax
observable has already been configured based on the bad state.
To see what happens, simplify things a bit and rewrite this epic like this:
const createInnerObservable = submitLoginAction => {
return Observable.of(generateDeviceId()).concat(login());
}
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN).mergeMap(createInnerObservable);
On arrival SUBMIT_LOGIN
, it mergeMap()
first calls the function createInnerObservable()
. The function must create a new observable and do this to call functions generateDeviceId()
and login()
. When called login()
, the state remains old, as at this point no internal observable has been created and therefore there was no way to send GENERATE_DEVICE_ID
. Because of this, it login()
returns an observable ajax
set up with the old data, and it becomes part of the resulting internal observable. Once it createInnerObservable()
returns, it mergeMap()
attaches to the returned internal observable and starts emitting values. GENERATE_DEVICE_ID
comes first, goes to the store, and the state changes. After that the observedajax
(which is now part of the internal observable) subscribes and makes a network request. But the new state does not affect this, since the ajax
observable is already initialized with the old data.
Wrapping login
in Observable.create
defers the call until the observable returned is subscribed to Observable.create
, and at that point the state has already been updated.
An alternative to this could be to add an additional epic that will react to the action GENERATE_DEVICE_ID
(or another, depending on what suits your domain) and send a login request, for example:
const submitLogin = payload => ({ type: "SUBMIT_LOGIN", payload });
// SUBMIT_LOGIN_REQUESTED is what used to be called SUBMIT_LOGIN
const submitLoginRequestedEpic = action$ =>
action$.ofType(SUBMIT_LOGIN_REQUESTED)
.mergeMap(({ payload }) => Rx.Observable.of(
generateDeviceId(uuidv1()),
submitLogin(payload))
);
const submitLoginEpic = (action$, store) =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({ payload }) => {
// explicitly pass all the data required to login
const { token, deviceId } = store.getState().user;
return login(payload.email, payload.password, token, deviceId)
.map(({ response }) => resolveLogin(response.content))
.concat(loadAbout());
});
Learning Resources
Since it is redux-observable
based on RxJS, it makes sense to get comfortable with Rx first.
I highly recommend watching "You Learn RxJS" by André Stalz. This should give an intuition of what the observables are and how they work under the hood.
André also wrote these great egg tutorials:
Also Jay Phelps gave a brilliant talk on redux-observable
, it's definitely worth a look.
source to share