Reputation: 33
So, I have an epic that receives a SUBMIT_LOGIN action and then it should fire the generateDeviceId function that returns an action with an id as payload. After that is processed by the reducer and the store is updated, it should request the login, then resolve it to store 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: login
function is an ajax
from rx-dom
that 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: uuidv1
function just generates a random key (its a lib)
I think (actually I'm sure) that I'm doing it wrong, but after two days i don't really know how to proceed. :/
After Sergey's first update I've changed my epic to that, but unfortunately for some reason rx-dom's
ajax is not working like Sergey's login$
observable. We're 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()])
After Sergey's second update I've changed my code again and ended up with a solution where I use two epics
and .concatMap
operator in order to synchronously
dispatch the actions and it 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()])
Upvotes: 3
Views: 1734
Reputation: 1761
If I got it right, you want your epic to produce the following sequence of actions in response to each SUBMIT_LOGIN
:
GENERATE_DEVICE_ID -- RESOLVE_LOGIN -- LOAD_ABOUT
Also, I guess that GENERATE_DEVICE_ID
needs to be issued immediately after SUBMIT_LOGIN
is received,
while RESOLVE_LOGIN
and LOAD_ABOUT
should be issued only after a stream returned by login()
emits.
If my guess is correct, then you just need to start the nested observable (the one created per each SUBMIT_LOGIN
)
with GENERATE_DEVICE_ID
action and startWith
operator 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 could be to use concat
operator: obs1.concat(obs2)
subscribes to the obs2
only when obs1
has completed.
Note also that if login()
needs to be called after GENERATE_DEVICE_ID
has been dispatched, you might want to 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 GENERATE_DEVICE_ID
is emitted before login()
is called, i.e. the sequence would be
GENERATE_DEVICE_ID -- login() -- RESOLVE_LOGIN -- LOAD_ABOUT
Update 2: The reason why login()
works not as expected is because it depends on an external state (const state = getCurrentState()
) which is different at the points in time when login()
is called and when an observable returned by login()
is subscribed to. AjaxRequest
captures the state at the point when login()
is called, which happens before GENERATE_DEVICE_ID
is dispatched to the store. At that point no network request is performed yet, but ajax
observable is already configured based on a wrong state.
To see what happens, let's simplify the things a bit and rewrite the epic this way:
const createInnerObservable = submitLoginAction => {
return Observable.of(generateDeviceId()).concat(login());
}
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN).mergeMap(createInnerObservable);
When SUBMIT_LOGIN
action arrives, mergeMap()
first calls createInnerObservable()
function. The function needs to create a new observable and to do that it has to call generateDeviceId()
and login()
functions. When login()
is called, the state is still old as at this point the inner observable has not been created and thus there was no chance for GENERATE_DEVICE_ID
to be dispatched. Because of that login()
returns an ajax
observable configured with an old data and it becomes a part of the resulting inner observable. As soon as createInnerObservable()
returns, mergeMap()
subscribes to the returned inner observable and it starts to emit values. GENERATE_DEVICE_ID
comes first, gets dispatched to the store and the state gets changed. After that, ajax
observable (which is now a part of the inner observable) is subscribed to and performs a network request. But the new state has no effect on that as ajax
observable has already been initialized with an old data.
Wrapping login
into an Observable.create
postpones the call until an observable returned by Observable.create
is subscribed to, and at that point the state is already up-to-date.
An alternative to that could be introducing an extra epic which would react to GENERATE_DEVICE_ID
action (or a different one, whichever suits your domain) and send a login request, e.g.:
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());
});
As redux-observable
is based on RxJS, it makes sense to get comfortable with Rx first.
I highly recommend watching "You will learn RxJS" talk by André Staltz. It should give an intuition of what observables are and how they work under the hood.
André has also authored these remarkable lessons on egghead:
Also Jay Phelps has given a brilliant talk on redux-observable
, it definitely worth watching.
Upvotes: 4