Эпик не реагирует на действия другого эпоса

У меня проблема с redux-observables. В моей ситуации одна эпопея ждет окончания другой эпопеи. Второй эпик может делать запрос или возвращать данные из кеша. Когда второй делает запрос, все работает как положено, но когда он возвращает кеш, первый не продолжается.

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;
  }
};

Вот эмуляция https://jsbin.com/qitutixuqu/1/edit?html,css,js,console,output. После нажатия на кнопку "Получить информацию о пользователе" в консоли появляется "Первый эпик", после второго нажатия на кнопку сообщения в консоли нет. Если вы добавите задержку к

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

он начинает работать, как и ожидалось.


person dz_navitski    schedule 15.07.2017    source источник
comment
К вашему сведению, я пытался помочь, но не мог понять, что ожидается и в чем именно проблема. Слишком много движущихся частей. JSbin также отличается от представленных примеров, что добавляет путаницы.   -  person jayphelps    schedule 17.07.2017


Ответы (1)


Краткий ответ: первый щелчок является асинхронным, возвращая задержку 1000 мс в fetchUserEpic. Второй щелчок — это полностью синхронное выполнение fetchUserEpic, в результате чего внутренний actions$.ofType(FETCH_USER_FULFILLED) пропускает действие в fetchUserEpic2.

Объяснение:

Трассируя fetchUserEpic в первый клик получаем вот это:

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

Проследив второй раз, получим:

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

Поскольку fetchUserEpic2 подписывается на actions$ в операторе switchMap, он не получает действия, которые уже были отправлены. redux-observable использует обычный Subject, а не ReplaySubject или аналогичный, поэтому, если действие отправляется до подписки, эта подписка на действия $ пропустит действие. По этой причине вам нужно быть осторожным, чтобы гарантировать, что действия отправляются асинхронно, когда вы зависите от внутренних подписок, таких как fetchUserEpic2.

Вот измененный исходный код с операторами ведения журнала трассировки:

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}`));
person meticoeus    schedule 18.07.2017