Как отправить несколько действий из избыточно-наблюдаемого?

Я хочу отправить несколько действий из эпоса, наблюдаемого при сокращении. Как мне это сделать? Я изначально начал с

const addCategoryEpic = action$ => {
  return action$.ofType(ADD_CATEGORY_REQUEST)
    .switchMap((action) => {
      const db = firebase.firestore()
      const user = firebase.auth().currentUser
      return db.collection('categories')
        .doc()
        .set({
          name: action.payload.name,
          uid: user.uid
        })
        .then(() => {
          return { type: ADD_CATEGORY_SUCCESS }
        })
        .catch(err => {
          return { type: ADD_CATEGORY_ERROR, payload: err }
        })
    })
}

Теперь вместо того, чтобы просто отправлять ADD_CATEGORY_SUCCESS, я также хочу обновить листинг (GET_CATEGORIES_REQUEST). Я пробовал много вещей, но всегда получаю

Действия должны быть простыми объектами. Используйте собственное промежуточное ПО для асинхронных действий

Например:

const addCategoryEpic = action$ => {
  return action$.ofType(ADD_CATEGORY_REQUEST)
    .switchMap((action) => {
      const db = firebase.firestore()
      const user = firebase.auth().currentUser
      return db.collection('categories')
        .doc()
        .set({
          name: action.payload.name,
          uid: user.uid
        })
        .then(() => {
          return Observable.concat([
            { type: ADD_CATEGORY_SUCCESS },
            { type: GET_CATEGORIES_REQUEST }
          ])
        })
        .catch(err => {
          return { type: ADD_CATEGORY_ERROR, payload: err }
        })
    })
}

Или изменить switchMap на mergeMap и т. д.


person Jiew Meng    schedule 25.12.2017    source источник


Ответы (5)


Проблема в том, что внутри вашего switchMap вы возвращаете обещание, которое само разрешается в concat Observable; Promise<ConcatObservable>. оператор switchMap прослушает обещание, а затем выдаст ConcatObservable как есть, который затем будет предоставлен store.dispatch под капотом с помощью redux-observable. rootEpic(action$, store).subscribe(store.dispatch). Поскольку отправка Observable не имеет смысла, поэтому вы получаете сообщение об ошибке, что действия должны быть простыми объектами.

То, что выдает ваш эпик, всегда должно быть простыми старыми объектами действий JavaScript, т. е. { type: string } (если у вас нет дополнительного промежуточного программного обеспечения для обработки других вещей)

Поскольку промисы всегда генерируют только одно значение, мы не можем использовать их для генерирования двух действий, нам нужно использовать Observables. Итак, давайте сначала преобразуем наш Promise в Observable, с которым мы можем работать:

const response = db.collection('categories')
  .doc()
  .set({
    name: action.payload.name,
    uid: user.uid
  })

// wrap the Promise as an Observable
const result = Observable.from(response)

Теперь нам нужно отобразить этот Observable, который будет выдавать одно значение, на несколько действий. Оператор map не выполняет операции "один ко многим", вместо этого мы будем использовать один из mergeMap, switchMap, concatMap или exhaustMap. В этом очень конкретном случае, какой из них мы выберем, не имеет значения, потому что Observable, к которому мы его применяем (который обертывает промис), будет когда-либо выдавать только одно значение, а затем будет завершено (). Тем не менее, очень важно понимать разницу между этими операторами, поэтому обязательно уделите некоторое время их изучению.

Я буду использовать mergeMap (опять же, в этом конкретном случае это не имеет значения). Поскольку mergeMap ожидает, что мы вернем «поток» (наблюдаемый объект, обещание, итератор или массив), я собираюсь использовать Observable.of для создания наблюдаемого из двух действий, которые мы хотим сгенерировать.

Observable.from(response)
  .mergeMap(() => Observable.of(
    { type: ADD_CATEGORY_SUCCESS },
    { type: GET_CATEGORIES_REQUEST }
  ))

Эти два действия будут запускаться синхронно и последовательно в том порядке, в котором я их указал.

Нам также нужно добавить обратную обработку ошибок, поэтому мы будем использовать оператор catch из RxJS — различия между ним и методом catch в промисе важны, но выходят за рамки этого вопроса.

Observable.from(response)
  .mergeMap(() => Observable.of(
    { type: ADD_CATEGORY_SUCCESS },
    { type: GET_CATEGORIES_REQUEST }
  ))
  .catch(err => Observable.of(
    { type: ADD_CATEGORY_ERROR, payload: err }
  ))

Соединяем все вместе, и у нас будет что-то вроде этого:

const addCategoryEpic = action$ => {
  return action$.ofType(ADD_CATEGORY_REQUEST)
    .switchMap((action) => {
      const db = firebase.firestore()
      const user = firebase.auth().currentUser
      const response = db.collection('categories')
        .doc()
        .set({
          name: action.payload.name,
          uid: user.uid
        })

      return Observable.from(response)
        .mergeMap(() => Observable.of(
          { type: ADD_CATEGORY_SUCCESS },
          { type: GET_CATEGORIES_REQUEST }
        ))
        .catch(err => Observable.of(
          { type: ADD_CATEGORY_ERROR, payload: err }
        ))
    })
}

Хотя это работает и отвечает на ваш вопрос, несколько редукторов могут вносить изменения в состояние из одного и того же единственного действия, что чаще всего следует делать вместо этого. Последовательное выполнение двух действий обычно является антишаблоном.

Тем не менее, как это часто бывает в программировании, это не абсолютное правило. Определенно бывают случаи, когда имеет смысл иметь отдельные действия, но это исключение. Вы лучше знаете, является ли это одним из тех исключительных случаев или нет. Просто имейте это в виду.

person jayphelps    schedule 27.12.2017

Почему вы используете Observable.concat? SwitchMap ожидает Observable или Promise, которые содержат значение (массив действий в нашем случае) из его функции обратного вызова. Поэтому нет необходимости возвращать Observable в обработчике успеха возвращенного обещания. Попробуй это:

const addCategoryEpic = action$ => {
  return action$.ofType(ADD_CATEGORY_REQUEST)
    .switchMap((action) => {
      const db = firebase.firestore()
      const user = firebase.auth().currentUser
      return db.collection('categories')
        .doc()
        .set({
          name: action.payload.name,
          uid: user.uid
        })
        .then(() => {
          return ([
            { type: ADD_CATEGORY_SUCCESS },
            { type: GET_CATEGORIES_REQUEST }
          ])
        })
        .catch(err => {
          return { type: ADD_CATEGORY_ERROR, payload: err }
        })
    })
}
person Alexander Poshtaruk    schedule 25.12.2017
comment
Я пробовал это, но все равно дает действия, которые должны быть простыми объектами. Используйте собственное промежуточное ПО для асинхронных действий - person Jiew Meng; 25.12.2017

Вы также можете попробовать использовать хранилище внутри своих эпиков.

const addCategoryEpic = (action$, store) => {
  return action$.ofType(ADD_CATEGORY_REQUEST)
    .switchMap((action) => {
      const db = firebase.firestore()
      const user = firebase.auth().currentUser
      const query = db.collection('categories')
        .doc()
        .set({
          name: action.payload.name,
          uid: user.uid
        })
       return Observable.fromPromise(query)
    }).map((result) => {
      store.dispatch({ type: ADD_CATEGORY_SUCCESS });
      return ({ type: GET_CATEGORIES_REQUEST })
   })
  .catch(err => {
     return { type: ADD_CATEGORY_ERROR, payload: err }
   })
}
person Denis Rybalka    schedule 25.12.2017
comment
Вызов store.dispatch из вашего эпика теперь устарел и будет удален, кстати. - person jayphelps; 27.12.2017
comment
О, пожалуйста, не делайте этого :) Спасибо за отличную библиотеку! - person Denis Rybalka; 28.12.2017

Для RxJs6:

action$.pipe(
  concatMap(a => of(Action1, Action2, Action3))
)

Обратите внимание, что у нас есть concatMap, mergeMap и switchMap, которые могут выполнять эту работу, различия: https://www.learnrxjs.io/operators/transformation/mergemap.html

person keos    schedule 21.06.2019

Я обнаружил, что должен использовать создание наблюдаемого с помощью Observable.fromPromise, а затем использовать flatMap для выполнения нескольких действий.

const addCategoryEpic = action$ => {
  return action$.ofType(ADD_CATEGORY_REQUEST)
    .switchMap((action) => {
      const db = firebase.firestore()
      const user = firebase.auth().currentUser
      const query = db.collection('categories')
        .doc()
        .set({
          name: action.payload.name,
          uid: user.uid
        })
      return Observable.fromPromise(query)
        .flatMap(() => ([
          { type: ADD_CATEGORY_SUCCESS },
          { type: GET_CATEGORIES_REQUEST }
        ]))
        .catch(err => {
          return { type: ADD_CATEGORY_ERROR, payload: err }
        })
    })
}
person Jiew Meng    schedule 25.12.2017