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()])

      

+3


source to share


1 answer


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.

+4


source







All Articles