Redux-Saga:サイドエフェクトの管理

Redux-Saga:サイドエフェクトの管理

ReactRedoxを活用して、Webアプリケーションを開発した際、非同期処理などのサイドエフェクト(Side effects)について試行錯誤を繰り返したことがありませんか?React、Redoxは、関数の応用、純粋関数、不変性などの関数型プログラミングを目指し、サイドエフェクトは望まれません。しかし「物事は思い通りにならない」という言葉を証明するように、サイドエフェクトなしでサービスを開発することは難しいでしょう。

React、Redoxの生態系は、関数型開発のように見えます。関数型の開発は(数学的)関数の応用で、関数に始めり関数で終わると考えられます。React生態系のほとんどはコンポーネントで、propから始まり、Reactエレメントを返すことで終わります。コンポーネントの根幹は関数であり、この生態系では関数(特に純粋関数)の規則を遵守してこそ認められます。Redoxも同様です。Action Creatorという純粋関数の戻り値(Action)を受け取り、Reducerと呼ばれる純粋な関数でデータを処理します。結論として、これらにサイドエフェクトは存在してはなりません。しかし前述の通り、サイドエフェクトのないサービス開発は存在しません。私たちが取り組むべくサイドエフェクトはどこかには存在します。そして、そのどこかが問題です。サイドエフェクトが入り込める隙間が1つありました。Redoxのミドルウェアで、これは本当に素晴らしい隙間です。Redux-Sagaは、この隙間においてサイドエフェクトを見事に管理します。

サイドエフェクト(Side Effect)

サイドエフェクトを翻訳すると「副作用」となります。ところが、React、Redoxとその生態系を調べていくと、サイドエフェクトが他のことを意味するように感じられました。単に副作用と解釈をすると何かおかしいようです。
Redux-SagaのREADMEには、次のような説明があります。

redux-saga is a library that aims to make application side effects(ie asynchronous things like data fetching and impure things like accessing the browser cache)easier to manage, more efficient to execute, simple to test, and better at handling failures.
redux-sagaはアプリケーションの「副作用」(データの要求(fetch)などの非同期操作、ブラウザのキャッシュのような純粋でないもの)を容易に管理し、効果的に実行し、簡単なテストとエラー処理を目的とする。

初めて読んだとき「データの要求、ブラウザのキャッシュがなぜ副作用になるのかな?」「本当はajaxも使ってはいけないのかな?」などと疑問に感じました。そこでサイドエフェクトについて調べました。

まず、英単語の意味から正しましょう。一般的に「Side Effect」は、望まない、否定的な、有害なものを内包します。そのため「副作用」と解釈される場合がほとんどです。しかし、コンピュータ工学では「副作用」ではなく、もう少し原始的な「付随効果」という意味が強いようです。「副作用」は「Negative Side Effect 」と称するのがもう少し正確です。

プログラミングあるいはコンピュータサイエンスのカテゴリにおいても、サイドエフェクトの定義はいくつかありますが、JavaScriptの観点から見ると、サイドエフェクトは(JavaScript)コードが外部の世界に影響を与えたり、または受けたりするものです。関数の観点で考えると、もう少し明確になります。関数の一貫性のある結果を保証できない、あるいは関数の外部に少しでも影響を与える場合もサイドエフェクトを持つと言えます。ただし、外部の世界を明確に定義することが難しいでしょう。コードの外側(outer)のスコープも外部の世界と言えますし、ユーザーのアクションやネットワーク通信も当然、外部の世界と言えます。

Redux-Saga

Redux-Sagaは最初からサイドエフェクトを管理するために作られました。Redoxが最初に登場したとき、アクションコンストラクタとReducerは純粋であるべきですが、サイドエフェクトはどのように処理するかについて多くの意見がありました。そしてRedux-Sagaが登場しました。

Redux-SagaのREADMEには、次のような内容があります。

The mental model is that a saga is like a separate thread in your application that’s solely responsible for side effects. redux-saga is a redux middleware, which means this thread can be started, paused and cancelled from the main application with normal redux actions, it has access to the full redux application state and it can dispatch redux actions as well.

Redux-Sagaは、アプリケーションで必要なサイドエフェクトを別スレッドに分離して管理ができ、Redoxのミドルウェアであり、Redoxのアクションを使って、スレッドの開始、停止、キャンセルさせることができるといいます。

Saga

Sagaについて少し調べてみると、Sagasという論文があり、GOTOカンファレンス-2015に「Applying Saga Pattern」という発表がありました。要約するとSagaは、任意のシステムで長期(Long lived)トランザクションと、その失敗処理をどのように管理するかという方法です。しかし、MSDNの「A Saga on Sagas」は少し異なります。CQRSパターンのプロセスマネージャーだと考えています。作業を効率的に処理することに、より関心を持ちます。Redux-Sagaは上記3つのすべてからインスピレーションを受けたといわれています。

たとえば、旅行サービスで、航空会社、宿泊施設、レンタカーの予約をしたと仮定しよう。ユーザーは単に「旅行プログラム」を予約しますが、サービス内部では飛行機、宿泊施設、レンタカーを一緒に予約します。Redux-Sagaの観点では、以下のような流れになります。

実際のサービスロジックはすべてSaga内部で処理し、その結果を再びアクションで発行(dispatch)します。この他 – 予約ボタンや予約の結果を表すコンポーネント、アクション、アクションコンストラクタ、Reducerなどすべて純粋関数でサイドエフェクトなしで実装できます。

Sagaの方式

Redux-Sagaを初めて見たとき、どのような概念かはある程度理解できましたが、RedoxのアクションとSagaをどのように構成して処理するのか分かりませんでした。effectchanneltaskblockingとnon-blockingasyncwatcherworkerforkspawnなどRedux-Sagaの用語だけでも覚えるのに時間がかかりました。今になってみると、もちろん用語と概念は重要ですが、実際にそれよりもまず行うことは、Sagaの流れ(Workflow)を理解することだと考えます。Sagaはアクションを演奏する演奏者のように見えます。Reduxアプリケーションはアクションによりデータ(state)が更新され、ビュー(View)が変化する。Sagaはこのアクションとデータ(state)間を演奏します。SagaのBeginner’s tutorialを見ると、Sagaの流れがもう少し分かりやすいでしょう。

1秒ごとにstateが1ずつ増加するアプリケーションには、INCREMENTINCREMENT_ASYNCの2つのアクションがあります。INCREMENTはReducerから受け取り直接処理するアクションでstate=+1コードを処理します。INCREMENT_ASYNCは、1秒後にstate=+1を処理しようとするアクションだが、Reducerは純粋関数という規則があるため、このアクションを直接処理できません。おおよその流れは次の通りです。

// 1. Dispatch Action 
  {
    type: INCREMENT_ASYNC
  }

// 2. Wait 1000ms
  delay(1000)

// 3. Dispatch Action
  {
    type: INCREMENT
  }

// 4. Reducer
  switch(action) {
    case INCREMENT: 
      return state + 1
    default:
      return state
  }

上記の手順で、2番、3番はSagaを利用して実装できます。

import { delay } from 'redux-saga' // 参考: delayは単に1秒後ResolveとなるPromiseである
import { put, takeEvery } from 'redux-saga/effects'

// INCREMENT_ASYNCアクションがDispatchになれば`incrementAsync`を実行するように登録する
export function* watchIncrementAsync() {
  yield takeEvery(INCREMENT_ASYNC, incrementAsync)
}

function* incrementAsync(action) {
  yield delay(1000)                 // 1秒待って
  yield put({ type: INCREMENT })    // INCREMENTアクションをDispatchする
}

これからRedoxを結合して考えると、次のようなフローになります。

注目すべき部分は1番でしょう。初めてRedux-Sagaを使ってコードを作成したとき、最も混乱した部分だったからです。Sagaでのみ特定のアクションを処理し、Reducerでそのアクションを処理しなかった場合、果たしてそのアクションはRedoxに到達するのか、到達するならいつ到達するのか気になりました。結論から言うとSagaを通じるすべてのアクションは、Reducerにまず到達します。Sagaのアクションを待って処理するコードは、次のような形で実装されます。

function sagaMiddleware({getState, dispatch}) {
  /* Saga初期化 .... */

  return next => action => {
    const result = next(action) // hit reducers  --- アクションはReducerに先に到達する
    sagaStdChannel.put(action)  // SagaにアクションがDispatchされることを知らせる
    return result
  }
}

もう少し正確に説明すると、Redux-sagaは過ぎ去ったアクションが実際にReducerに到達したか、途中で変形やフィルタリングされたかまでは分かりません。つまり、アクションがDispatchされる過程そのものには関与しません。アクションが通り過ぎるのを傍観(watching)した後、彼らだけの演奏をします。これがSagaの方式です。

  1. すべてのアクションはまずReducerに到達する(実際はアクションが通過するのを見てから動作する)
  2. 過去のアクションを自分のチャネルに知らせる
  3. アクション別に処理を実行し、必要に応じてその結果を再実行(= dispatch)する

まとめ

上記、1~4までのRedux-Sagaのフローを理解すれば、Redux-Sagaを使う準備はできました。しかしこれまでの説明は、Redux-Sagaの氷山の一角にもなりません。SagaのTask概念や、EffectChannel概念などは、これから複雑なアプリケーションを実装する際に不可欠な要素であり、実際にその内容をここで説明できず残念です。この記事を読んでRedux-Sagaに少しでも興味がわいたなら公式文書を読んでみよう。内容がきちんと整理されており、理解する上で非常に役立つと思います。

アプリケーションを開発し、サイドエフェクト、非同期、あるいはこれ以外のもの –

  1. 並列処理
  2. 失敗、エラーの一括処理
  3. Reactコンポーネントのパフォーマンス(一部の状況でコンポーネントのReconciliationを防止できる)
  4. 非同期コードを含むサービスコードのテスト
  5. Socket連動

などを検討していけば、Redux-Sagaは本当に良い選択だと思います。

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop