web-dev-qa-db-ja.com

Reduxで複雑な副作用を処理する方法

私はこの問題の解決策を見つけるのに何時間も苦労してきました...

オンラインスコアボードを使用してゲームを開発しています。プレーヤーはいつでもログインおよびログアウトできます。ゲームを終了すると、プレーヤーはスコアボードと自分のランクを確認し、スコアが自動的に送信されます。

スコアボードには、プレーヤーのランキングとリーダーボードが表示されます。

Screenshot

スコアボードは、ユーザーがプレイを終了する(スコアを送信する)ときと、ランキングを確認するだけのときに使用されます。

これは、ロジックが非常に複雑になるところです。

  • ユーザーがログインしている場合、スコアが最初に送信されます。新しいレコードが保存された後、スコアボードがロードされます。

  • それ以外の場合、スコアボードはすぐにロードされます。プレイヤーには、ログインまたは登録するオプションが与えられます。その後、スコアが送信され、スコアボードが再度更新されます。

  • ただし、提出するスコアがない場合(ハイスコア表を表示するだけ)。この場合、プレーヤーの既存のレコードは単にダウンロードされます。ただし、このアクションはスコアボードに影響しないため、スコアボードとプレーヤーのレコードの両方を同時にダウンロードする必要があります。

  • レベルの数に制限はありません。各レベルには異なるスコアボードがあります。ユーザーがスコアボードを表示すると、ユーザーはそのスコアボードを「観察」しています。閉じられると、ユーザーは監視を停止します。

  • ユーザーはいつでもログインおよびログアウトできます。ユーザーがログアウトすると、ユーザーのランキングは消え、ユーザーが別のアカウントとしてログインすると、そのアカウントのランキング情報が取得されて表示されます。

    ...しかし、この情報の取得は、ユーザーが現在監視しているスコアボードに対してのみ行われるべきです。

  • 表示操作の場合、ユーザーが同じスコアボードに再サブスクライブした場合にフェッチが行われないように、結果はメモリにキャッシュされる必要があります。ただし、提出されるスコアがある場合、キャッシュは使用しないでください。

  • これらのネットワーク操作はいずれも失敗する可能性があり、プレーヤーはそれらを再試行できる必要があります。

  • これらの操作はアトミックである必要があります。すべての状態を一度に更新する必要があります(中間状態なし)。

現在、私はBacon.js(関数型リアクティブプログラミングライブラリ)を使用してこれを解決できます。アトミックアップデートのサポートが付属しているためです。コードは非常に簡潔ですが、現時点では、予測不可能な乱雑なスパゲッティコードです。

Reduxを見始めました。だから私はストアを構造化しようとし、次のようなものを思いつきました(YAMLish構文で):

_user: (user information)
record:
  level1:
    status: (loading / completed / error)
    data:   (record data)
    error:  (error / null)
scoreboard:
  level1:
    status: (loading / completed / error)
    data:
      - (record data)
      - (record data)
      - (record data)
    error:  (error / null)
_

問題は次のとおりです:副作用をどこに置くか。

副作用のないアクションの場合、これは非常に簡単になります。たとえば、LOGOUTアクションでは、recordレデューサーは単純にすべてのレコードを消去できます。

ただし、一部のアクションには副作用があります。たとえば、スコアを送信する前にログインしていない場合、正常にログインすると、_SET_USER_アクションによってユーザーがストアに保存されます。

ただし、送信するスコアがあるため、この_SET_USER_アクションはAJAX要求を発生させると同時に、_record.levelN.status_をloading

問題は:どのように副作用(スコアの提出)を記録するときに起こるべきであることを意味しますか?アトミックな方法で?

Elmアーキテクチャでは、アップデーターはAction -> Model -> (Model, Effects Action)の形式を使用するときに副作用を発生させることもできますが、Reduxでは、_(State, Action) -> State_のみです。

非同期アクション ドキュメントから、彼らが推奨する方法は、アクション作成者にそれらを置くことです。これは、ログインアクションを成功させるために、スコアを送信するロジックもアクションクリエーターに配置する必要があることを意味しますか?

_function login (options) {
  return (dispatch) => {
    service.login(options).then(user => dispatch(setUser(user)))
  }
}

function setUser (user) {
  return (dispatch, getState) => {
    dispatch({ type: 'SET_USER', user })
    let scoreboards = getObservedScoreboards(getState())
    for (let scoreboard of scoreboards) {
      service.loadUserRanking(scoreboard.level)
    }
  }
}
_

この連鎖反応の原因となるコードが2つの場所に存在するため、これは少し奇妙に感じます。

  1. 減速機で。 _SET_USER_アクションがディスパッチされると、recordレデューサーは、観察されたスコアボードに属するレコードのステータスをloadingに設定する必要もあります。
  2. アクションクリエーターで、スコアの取得/送信の実際の副作用を実行します。

また、アクティブなすべてのオブザーバーを手動で追跡する必要があるようです。 Bacon.jsバージョンでは、次のようなことをしました。

_Bacon.once() // When first observing the scoreboard
.merge(resubmit口) // When resubmitting because of network error
.merge(user川.changes().filter(user => !!user).first()) // When user logs in (but only once)
.flatMapLatest(submitOrGetRanking(data))
_

上記のすべての複雑なルールのために、実際のベーコンコードははるかに長くなり、ベーコンバージョンがかろうじて読みやすくなりました。

しかし、ベーコンはすべてのアクティブなサブスクリプションを自動的に追跡しました。これにより、これをReduxに書き換えるには多くの手動操作が必要になるため、切り替えの価値はないのではないかと疑問に思うようになりました。誰かがポインターを提案できますか?

45
Thai

Edit:これらのアイデアに触発された redux-saga プロジェクトがあります

いくつかの素敵なリソースがあります


Flux/Reduxは、バックエンドイベントストリーム処理からインスピレーションを得ています(名前が何であれ、イベントソーシング、CQRS、CEP、ラムダアーキテクチャなど)。

ActionCreators/Actions of Fluxをコマンド/イベント(バックエンドシステムで通常使用される用語)と比較できます。

これらのバックエンドアーキテクチャでは、多くの場合 Saga またはProcess Managerと呼ばれるパターンを使用します。基本的には、イベントを受信し、独自の状態を管理し、新しいコマンドを発行するシステムの一部です。簡単にするために、これはIFTTT(If-This-Then-That)の実装に少し似ています。

これはFRPとBaconJSで実装できますが、Reduxレデューサーの上に実装することもできます。

function submitScoreAfterLoginSaga(action, state = {}) {  
  switch (action.type) {

    case SCORE_RECORDED:
      // We record the score for later use if USER_LOGGED_IN is fired
      state = Object.assign({}, state, {score: action.score}

    case USER_LOGGED_IN: 
      if ( state.score ) {
        // Trigger ActionCreator here to submit that score
        dispatch(sendScore(state.score))
      } 
    break;

  }
  return state;
}

それを明確にするために:減速機から駆動まで計算された状態Reactレンダリングは絶対に純粋なままである必要があります! 、そしてこれは、アプリの異なる分離部分を複雑なルールと同期する必要がある場合です。この「サガ」の状態は、React rendering!

Reduxはこのパターンをサポートするものを提供するとは思いませんが、おそらく自分で簡単に実装できます。

スタートアップフレームワーク でこれを実行しましたが、このパターンは正常に機能します。次のようなIFTTTを処理できます。

  • ユーザーのオンボーディングがアクティブで、ユーザーがPopup1を閉じてからPopup2を開き、ヒントのヒントを表示します。

  • ユーザーがモバイルWebサイトを使用してMenu2を開き、Menu1を閉じるとき

[〜#〜] important [〜#〜]:Reduxのようないくつかのフレームワークのアンドゥ/リドゥ/リプレイ機能を使用している場合、それは重要ですイベントログの再生中は、再生中に新しいイベントを発生させたくないため、これらすべてのサガは配線されていません!

17