dz_navitski
dz_navitski

Reputation: 33

Epic doesn't react on action from another epic

I have a problem with redux-observables. In my situation one epic wait for ending of another epic. The second epic can make a request or return data from the cache. When second makes the request all work as expected, but when it returns cache the first one doesn't continue.

const { Observable } = Rx;

const FETCH_USER = 'FETCH_USER';
const FETCH_USER_FULFILLED = 'FETCH_USER_FULFILLED';
const FETCH_USER2 = 'FETCH_USER2';
const FETCH_USER_FULFILLED2 = 'FETCH_USER_FULFILLED2';
const FETCH_USER_REJECTED = 'FETCH_USER_REJECTED';
const FETCH_USER_CANCELLED = 'FETCH_USER_CANCELLED';

const fetchUser = id => ({ type: FETCH_USER, payload: id });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
const fetchUser2 = id => ({ type: FETCH_USER2, payload: id });
const fetchUserFulfilled2 = payload => ({ type: FETCH_USER_FULFILLED2, payload });
const cancelFetchUser = () => ({ type: FETCH_USER_CANCELLED });

let isFetchced = false;

const fakeAjax = url =>
  Observable.of({
    id: url.substring(url.lastIndexOf('/') + 1),
    firstName: 'Bilbo',
    lastName: 'Baggins'
  }).delay(1000);

const fakeAjax2 = url =>
  Observable.of({
    id: url.substring(url.lastIndexOf('/2') + 1),
    firstName: 'Bilbo2',
    lastName: 'Baggins2'
  }).delay(1000);

const fetchUserEpic = (action$, store) =>
  action$.ofType(FETCH_USER)
    .mergeMap(action => {
      const observable = isFetchced ? Observable.of({
        id: 2,
        firstName: 'Bilbo',
        lastName: 'Baggins'
      }) : fakeAjax(`/api/users/${action.payload}`);
      isFetchced = true;
      console.log(action);
      return observable
        .map(response => fetchUserFulfilled(response))
        .takeUntil(action$.ofType(FETCH_USER_CANCELLED))
    });

const fetchUserEpic2 = action$ =>
  action$.ofType(FETCH_USER2)
    .switchMap(() => action$.ofType(FETCH_USER_FULFILLED)
               .take(1)
    .mergeMap(() => {
        console.log("First epic");
        return fakeAjax2(`/api/users/${1}`)
            .map(response => fetchUserFulfilled2(response))
    }).startWith(fetchUser('redux-observable')));

const users = (state = {}, action) => {
  switch (action.type) {
    case FETCH_USER_FULFILLED:
      return {
        ...state,
        [action.payload.id]: action.payload
      };

    default:
      return state;
  }
};

const isFetchingUser = (state = false, action) => {
  switch (action.type) {
    case FETCH_USER:
      return true;

    case FETCH_USER_FULFILLED:
    case FETCH_USER_CANCELLED:
      return false;

    default:
      return state;
  }
};

Here is emulation https://jsbin.com/qitutixuqu/1/edit?html,css,js,console,output. After clicking on the button "Fetch user info" in the console you can see "First epic", after the second click on the button there is no message in console. If you add delay to

Observable.of({
  id: 2,
  firstName: 'Bilbo',
  lastName: 'Baggins'
}).delay(10)

it starts work as expected.

Upvotes: 3

Views: 1018

Answers (1)

meticoeus
meticoeus

Reputation: 553

Short answer: The first click is asynchronous by returning a delay of 1000 ms in fetchUserEpic. The second click is a fully synchronous execution of fetchUserEpic which results in the inner actions$.ofType(FETCH_USER_FULFILLED) missing the action in fetchUserEpic2.

Explanation:

Tracing fetchUserEpic in the first click we get this:

fetchUserEpic src: FETCH_USER2
fetchUserEpic2 src: FETCH_USER2
fetchUserEpic2 in: FETCH_USER2
fetchUserEpic2 out: FETCH_USER
fetchUserEpic src: FETCH_USER
fetchUserEpic in: FETCH_USER
fetchUserEpic2 src: FETCH_USER <- Notice location
fetchUserEpic out: FETCH_USER_FULFILLED
fetchUserEpic src: FETCH_USER_FULFILLED
fetchUserEpic2 src: FETCH_USER_FULFILLED
fetchUserEpic2-inner src: FETCH_USER_FULFILLED <- now subscribed
fetchUserEpic2-inner in: FETCH_USER_FULFILLED
First epic
fetchUserEpic2 out: FETCH_USER_FULFILLED2
fetchUserEpic src: FETCH_USER_FULFILLED2
fetchUserEpic2 src: FETCH_USER_FULFILLED2

Tracing the second time we get:

fetchUserEpic src: FETCH_USER2
fetchUserEpic2 src: FETCH_USER2
fetchUserEpic2 in: FETCH_USER2
fetchUserEpic2 out: FETCH_USER
fetchUserEpic src: FETCH_USER
fetchUserEpic in: FETCH_USER
fetchUserEpic out: FETCH_USER_FULFILLED
fetchUserEpic src: FETCH_USER_FULFILLED
fetchUserEpic2 src: FETCH_USER_FULFILLED
fetchUserEpic2 src: FETCH_USER <- Notice location

Since fetchUserEpic2 subscribes to to actions$ in the switchMap statement, it does not receive actions that were already dispatched. redux-observable uses a regular Subject, not a ReplaySubject or similar so if the action is dispatched before the subscription, that actions$ subscription will miss the action. For this reason you need to be careful to guarantee that actions are dispatched asynchronously when you are depending on inner subscriptions like fetchUserEpic2 is using.

Here is the modified source with the tracing logging statements:

const fetchUserEpic = (action$, store) =>
  action$
    .do(a => console.log(`fetchUserEpic src: ${a.type}`))
    .ofType(FETCH_USER)
    .do(a => console.log(`fetchUserEpic in: ${a.type}`))
    .mergeMap(action => {
      const observable = isFetchced ? Observable.of({
        id: 2,
        firstName: 'Bilbo',
        lastName: 'Baggins'
      }) : fakeAjax(`/api/users/${action.payload}`);
      return observable
        .map(response => (isFetchced = true,fetchUserFulfilled(response)))
        .takeUntil(action$.ofType(FETCH_USER_CANCELLED))
    })
    .do(a => console.log(`fetchUserEpic out: ${a.type}`));

const fetchUserEpic2 = action$ =>
  action$
    .do(a => console.log(`fetchUserEpic2 src: ${a.type}`))
    .ofType(FETCH_USER2)
    .do(a => console.log(`fetchUserEpic2 in: ${a.type}`))
    .switchMap(() =>
      action$
        .do(a => console.log(`fetchUserEpic2-inner src: ${a.type}`))
        .ofType(FETCH_USER_FULFILLED)
        .do(a => console.log(`fetchUserEpic2-inner in: ${a.type}`))
        .take(1)
        .do(() => console.log("First epic"))
        .mergeMap(() =>
          fakeAjax2(`/api/users/${1}`)
            .map(response => fetchUserFulfilled2(response))
        ).startWith(fetchUser('redux-observable')))
    .do(a => console.log(`fetchUserEpic2 out: ${a.type}`));

Upvotes: 3

Related Questions