
Redux Sagaのコンセプトを短いコードで解説! React
本日は、Redux Sagaのコンセプトを短いコードで解説する記事を書きます。
[/st-kaiwa1]
sagaとは?
マイクロサービスの世界では、
sagaは複数のサービスやビジネスドメインにまたがるトランザクションの実装を支援するためのものです。
sagaパターンは、アプリケーション内の副作用を処理する方法として設計されています。
Redux Sagaのホームページでは、このフレームワークを次のように説明しています。
アプリケーションの副作用(データ取得などの非同期処理や、ブラウザのキャッシュへのアクセスなどの不純な処理)の管理を容易にし、実行効率を高め、テストを容易にし、障害の処理を容易にすることを目的としたライブラリです。
callback-hellにかなりの時間を費やしてきたJavaScript開発者としては、これは確かに良さそうですね。
例として、ユーザーがアイテムをバスケットに追加するイベントを考えてみましょう。
Reduxの状態では、下記のようにアクセスしなければなりません。
- メニューコンテキストで、メニューアイテムの情報を取得
- レストランコンテキストでは、商品の価格を決定します。価格は配達時と集荷時で異なる場合がある。
- アイテムを追加して合計金額を増やすためのバスケットコンテキスト。
- エラーが発生した場合のエラーコンテキスト(さらに、使用したいロギングサービスもある。)
ジェネレーター機能についての簡単な説明
Redux Sagaでは、ジェネレータ関数を多用しています。
このフレームワークを使っているときは、間違いなくジェネレーター関数を最も多く使っています。
この記事ではジェネレーター関数について深く掘り下げるつもりはありませんが、
Redux Sagaの基本を理解するためには、この特別な種類の関数についていくつか知っておく必要があります。
ジェネレーター関数を使うと、関数を一時停止してプロセスの終了を待つことができます。
これは、プロミスを解決するための思考プロセスに似ています。
ジェネレータ関数の基本的な構文は次のようになります。
function* myGenerator() {
const result = yield myAsyncFunc1()
yield myAsyncFunc2(result)
}
ジェネレーター関数は、functionキーワードの後にアスタリスク*を付けて宣言します。
yield キーワードは、その行の実行が終了するまで待つように関数に指示します。
この場合、myGenerator は myAsyncFunc1 が完了するまで待ち、
その時点で関数は myAsyncFunc2 の実行に移り、最初の関数から返された結果を引き継ぎます。
今回説明した以外にもジェネレーター関数にはたくさんの機能がありますが、
この記事とRedux Sagaの基本を理解するにはこれで十分です。
例題
今回の例では、食べ物を注文するアプリケーションを構築するための基礎を検討します。
この種のアプリには、考慮しなければならない特定のビジネスドメインがあります。
このようなロジックの違いを「コンテクストの違い」と呼ぶことにします。
- メニュー:アイテム、カテゴリー、オプションやサイドメニュー、サイズ、価格、アレルゲン情報など。
- チェックアウト:選択した商品のバスケット、合計金額、税金、配送料、支払い方法、クーポン券など
- ユーザー:保存された支払い方法、保存された住所、電子メール、電話番号など
ここでは、アプリのアーキテクチャの中で、
これらのシナリオをどのように計画するか、ハイレベルな概要を見ていきます。
それぞれのコンテキストでどのようなイベントが必要なのか?
sagaの中で処理したい副作用は何か?
これらは、sagaのレイアウトを設計する際に必要となる質問です。
ルートsaga
Reduxのreducerが、他のreducerを結合するルートreducerを持っているという点で、構成されているのと同様に、
sagaもルートsagaを起点として構成されています。
function* rootSaga() {
yield all([
menuSaga(),
checkoutSaga(),
userSaga()
])
}
まず、目に飛び込んできそうなところに注目してみましょう。
rootSagaは、チェーンのベースとなるSagaです。
menuSaga, checkoutSaga, userSaga は、いわゆるスライス・サーガです。
それぞれがsagaツリーの1つのセクション(またはスライス)を処理します。
all()はredux-sagaがエフェクトクリエイターと呼ぶものです。
これらは基本的に、sagaを(ジェネレータ関数と一緒に)に動作させるために使用する関数です。
各エフェクトクリエーターは、redux-sagaのミドルウェアで使用されるオブジェクト(エフェクトと呼ばれる)を返します。
Reduxのアクションやアクションクリエーターと名前が似ていることに注意してください。
redux-sagaにはエフェクトクリエーターの長いリストがありますけども、
ここでは、all()がエフェクト・クリエーターで、渡されたすべてのsagaを同時に実行し、
すべてのsagaが完了するのを待つようにsagaに指示しています。
ドメインロジックをカプセル化したsagaの配列を渡します。
ウォッチャー・サガ
では、サブsagaの基本的な構成を見てみましょう。
import { put, takeLatest } from 'redux-saga/effects'
function* fetchMenuHandler() {
try {
// Logic to fetch menu from API
} catch (error) {
yield put(logError(error))
}
}
function* menuSaga() {
yield takeLatest('FETCH_MENU_REQUESTED', fetchMenuHandler)
}
ここではmenuSagaが表示されていますが、これは先ほどのスライスSagaの一つです。
ここでは、ストアにディスパッチされるさまざまなアクションタイプをリッスンしています。
menuSaga は takeLatest を使用してアクションタイプを監視しており、そのアクションタイプを見つけると、
ハンドラ関数 fetchMenuHandler を実行します。
このような理由から、このタイプのsagaはウォッチャーサガと呼ばれています。
要約すると、ウォッチャー・sagaはアクションをリッスンし、ハンドラー・sagaをトリガーします。
ハンドラ関数の本体をtry/catchブロックで囲み、非同期処理中に発生したエラーを処理できるようにしています。
put()は基本的にReduxのdispatchメソッドに相当します。
アプリケーションでは、おそらく別のエラー処理メカニズムを追加することになるでしょう。
const logError = error => ({
type: 'LOG_ERROR',
payload: { error }
})
fetchMenuHandlerにロジックを追加してみましょう。
function* fetchMenuHandler() {
try {
const menu = yield call(myApi.fetchMenu)
yield put({ type: 'MENU_FETCH_SUCCEEDED', payload: { menu } ))
} catch (error) {
yield put(logError(error))
}
}
ここでは、HTTPクライアントを使用して、メニューデータAPIにリクエストを行います。
アクションではなく個別の非同期関数を呼び出す必要があるので、call()を使います。
引数を渡す必要がある場合は、call()の後続の引数として渡します。
私たちのジェネレータ関数 fetchMenuHandler は、yield を使用して、
myApi.fetchMenu がレスポンスを取得するのを待つ間、
自分自身を一時停止します。
その後、put()で別のアクションをディスパッチして、ユーザーにメニューをレンダリングします。
さて、これらの概念をまとめて、もうひとつのサブsagaである「チェックアウトサーガ」を作りましょう。
import { put, select, takeLatest } from 'redux-saga/effects'
function* itemAddedToBasketHandler(action) {
try {
const { item } = action.payload
const onSaleItems = yield select(onSaleItemsSelector)
const totalPrice = yield select(totalPriceSelector)
if (onSaleItems.includes(item)) {
yield put({ type: 'SALE_REACHED' })
}
if ((totalPrice + item.price) >= minimumOrderValue) {
yield put({ type: 'MINIMUM_ORDER_VALUE_REACHED' })
}
} catch (error) {
yield put(logError(error))
}
}
function* checkoutSaga() {
yield takeLatest('ITEM_ADDED_TO_BASKET', itemAddedToBasketHandler)
}
バスケットに商品が追加されると、いくつかのチェックや検証が必要になることが想像できます。
ここでは、ユーザーが商品を追加したことで売上の対象となったかどうか、
またはユーザーが注文に必要な最低注文金額に達したかどうかをチェックしています。
Redux Sagaは、副作用を処理するためのツールであることを忘れないでください。
実際にアイテムをバスケットに追加するロジックを、ここに格納するためには必ずしも使用すべきではありません。
そのための処理は通常はreducerを使います。
なぜなら、それがずっとシンプルなreducerパターンが完璧に適しているからです。
ここでは新しい効果であるselect()を利用しています。
selectにセレクタを渡すと、sagaの中からReduxストアの一部を取得します。
これは、1つのsagaの中で複数のコンテキストに依存している場合に非常に便利です。
セレクタとは、Reduxで利用される一般的なデザインパターンで、状態を渡されて、
その状態の小さな部分を単純に返す関数を作成します。例えば、以下のようなものです。
const onSaleItemsSelector = state => state.onSaleItems
const basketSelector = state => state.basket
const totalPriceSelector = state => basketSelector(state).totalPrice
セレクターは、ここの値にに手を伸ばし、その一部を手に入れるための信頼できる一貫した方法です。
結論
大したコードではありませんが、食べ物を注文するアプリのイベントを処理するための構造を作ることができました。
アプリケーションの次のドメインを導入したいときは、新しいウォッチャーサーガを作成して、それをルートサーガに渡します。
そして、アクションをリッスンしたり、サイドエフェクトを実行するハンドラを作成します。
Reduxのフローに新しい(副作用の)効果を導入するために、
複雑なメンタルモデルやスパゲッティコードを作成する必要がないので、
この設定をスケーリングすることは簡単で論理的です。
Redux Sagaは、アプリケーションで発生する様々な変更や副作用を管理するための優れたフレームワークです。
エフェクトと呼ばれる非常に便利なヘルパーメソッドが用意されており、アクションをディスパッチしたり、ステートの一部を取得したりすることができます。
今後の記事では、デモアプリケーションを作成し、より高度なコンセプトを紹介していきます。
この記事が良い入門書となり、皆さんがRedux Sagaを使い始めるためのしっかりとした基礎を身につけられたことを願っています。
[st-minihukidashi fontawesome=”” fontsize=”” fontweight=”” bgcolor=”#FFB74D” color=”#fff” margin=”0 0 20px 0″ radius=”” add_boxstyle=””]この記事は英語の記事でとても良い記事があったので、要点だけ絞って分かりやすく翻訳しました。[/st-minihukidashi]