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

公開日:2018-10-16
最終更新:2018-10-24
※この記事は外部サイト(https://qiita.com/terrierscript/items/0fee...)からのクロス投稿です

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

中間にActionをつくる

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

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

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

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

// ボタンからPINGのActionが来たら中間のTEMP_PINGを発火
const seedEpic = (action$) =>
  action$.pipe(
    ofType("PING"),
    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)
記事が少しでもいいなと思ったらクラップを送ってみよう!
18
+1
@terrierの技術ブログ

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする