Help us understand the problem. What is going on with this article?

redux-observableで複数のActionを発火する方法をたくさん考える

More than 1 year has passed since last update.

redux-observableで1つのactionから最終的に複数のactionを発火する方法を毎回ググってるのでメモる

中間にActionをつくる

これが一番スタンダードだと思われる。8割は解決するはず。

今回は例えばこんなボタンを用意した時を考える

const Button = ({ dispatch }) => (
  <button onClick={(e) => dispatch({ type: "PING" })}>
    Ping
  </button>
)

中間Actionを用意する場合のEpicはこうなる。

// ボタンからPINGのActionが来たら中間のTEMP_PINGを発火
const seedEpic = (action$) =>
  action$.pipe(
    ofType("PING"),
    filter(action => !!action.payload), // 例としてここでは事前処理のepicでfilterをしてみる
    map((action: any) => ({
      type: "TEMP_PING",
      payload: action.payload
    }))
  )

// TEMP_PINGを受けて値を2倍にするEpic
const doubleEpic = (action$) =>
  action$.pipe(
    ofType("TEMP_PING"),
    map((action: any) => ({
      type: "PONG",
      payload: action.payload * 3
    }))
  )

// TEMP_PINGを受けて値を3倍にするEpic
const tripleEpic = (action$) =>
  action$.pipe(
    ofType("TEMP_PING"),
    map((action: any) => ({
      type: "PUNG",
      payload: action.payload * 4
    }))
  )

export const pingEpic = combineEpics(debug(), seedEpic, doubleEpic, tripleEpic)

mergeMapからの配列で返す

単純に複数actionを一関数中で発火出来るならmergeMapと配列を使う事でも実現出来る

const pingEpic = (action$) =>
  action$.pipe(
    ofType("PING"),
    mergeMap((action: any) => {
      return [
        {
          type: "PONG",
          payload: action.payload * 2
        },
        {
          type: "PUNG",
          payload: action.payload * 3
        }
      ]
    })
  )

Partitionで分離する(できる場合)

ここまでは「どっちも発火したい」であったが条件によって変えるならpartitionで十分だろう

export const pingEpic = (action$) => {
  const [even, odd] = action$.pipe(
    ofType("PING"),
    partition((action: any) => action.payload % 2 === 0)
  )
  return merge(
    // 偶数の場合2倍
    even.pipe(
      map((action: any) => ({
        type: "PONG",
        payload: action.payload * 2
      })),
    ),
    // 奇数の場合3倍
    odd.pipe(
      map((action: any) => ({
        type: "PUNG",
        payload: action.payload * 3
      }))
    )
  )
}

おまけ:Rxを駆使する

ここから下はほぼ使うことはない。思考実験に近い。
一応こういうのもあるという紹介

mergeMapとofで対応する

こんな感じでmergeMapを使う事が出来る
後述するがこれは冗長だろう

export const pingEpic = (action$) =>
  action$.pipe(
    ofType("PING"),
    mergeMap(action => {
      const source$ = of(action)
      return merge(
        source$.pipe(
          map((action: any) => ({
            type: "PONG",
            payload: action.payload * 2
          })),
        ),
        source$.pipe(
          map((action: any) => ({
            type: "PUNG",
            payload: action.payload * 3
          }))
        )
      )
    })
  )

mergeで2つを組み合わせる

上記の冗長さは下記のように分岐するとこまでをこんな感じで切り出して後でmergeするやり方もある。partitionでのやり方に近い

export const pingEpic = (action$) => {
  // ここまでが共通
  const source$ = action$.pipe(
    ofType("PING"),
  )
  // 分岐するsourceをそれぞれ Observable.mergeで混ぜる。
  return merge(
    source$.pipe(
      map((action: any) => ({
        type: "PONG",
        payload: action.payload * 2
      })),
    ),
    source$.pipe(
      map((action: any) => ({
        type: "PUNG",
        payload: action.payload * 3
      })),
    )
  )
}

例えばこれだと下記のように「連打されたActionをただログ出すだけと連打を検知するものに分けたい」ようなことも可能(使い所は不明)

export const pingEpic = (action$) => {
  const source$ = action$.pipe(
    ofType("PING"),
  )
  return merge(
    source$.pipe( // こっちはLOG出力だけ
      map((action: any) => ({
        type: "LOG",
        payload: action.payload
      })),
    ),
    source$.pipe(
      bufferTime(1000), // 連打
      filter((items: any[]) => items.length > 0),
      map((actions: any[]) => { // 受け取ったactionが配列になる
        return {
          type: "MASH",
          payload: actions.length
        }
      })
    )
  )
}

別途Subjectを作る

更にやらないであろう手法。
思いついたのでせっかくなので供養する。

// Subjectを別に作る。
// 本来redux-observableがSubjectを管理しているので、ここにわざわざ作るのはいったい、という感じ
const pongSubject = new Subject()
const pungSubject = new Subject()

const mainEpic = (action$) => {
  return action$.pipe(
    ofType("PING"),
    // tapでそれぞれsubjectを発火
    tap((action: any) => pongSubject.next({ type: "PONG", payload: action.payload * 2 })),
    tap((action: any) => pungSubject.next({ type: "PUNG", payload: action.payload * 3 })),
    ignoreElements() // tapだけでそのまま返すと無限PINGで死ぬ
  )
}

// SubjectをasObservableで変換してEpicと扱う
const pongEpic = () => pongSubject.asObservable()
const pungEpic = () => pungSubject.asObservable()

export const pingEpic = combineEpics(mainEpic, pongEpic, pungEpic)

使い所は殆どないが、例えばどうしてもメインの処理があってそこにうまくログのような処理をはさみたい場合とかがあれば使いどころになるかもしれない

const logSubject = new Subject()

const mainEpic = (action$) => {
  return action$.pipe(
    ofType("PING"),
    tap((action: any) => logSubject.next({ type: "LOG", payload: action.payload })),
    bufferTime(500), // 連打
    filter((items: any[]) => items.length > 0),
    map((actions: any[]) => { // 受け取ったactionが配列になる
      return {
        type: "MASH",
        payload: actions.length
      }
    }),
  )
}
const logEpic = () => logSubject.asObservable()

export const pingEpic = combineEpics(mainEpic, logEpic)
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした