web-dev-qa-db-ja.com

タイムアウト付きでReduxアクションをディスパッチする方法

アプリケーションの通知状態を更新するアクションがあります。通常、この通知はエラーまたはある種の情報になります。 5秒後に通知状態を最初の状態に戻すため、別のアクションをディスパッチする必要があるため、通知はありません。この主な理由は、5秒後に通知が自動的に消える機能を提供することです。

私はsetTimeoutを使用して別のアクションを返すことに成功しておらず、これがオンラインでどのように行われているのかわかりません。だからどんなアドバイスでも大歓迎です。

756
Ilja

ライブラリがすべてを行う方法を規定するべきだと思うという落とし穴 に陥らないでください。 JavaScriptでタイムアウトを設定して何かをしたい場合は、setTimeoutを使用する必要があります。 Reduxのアクションが異なる理由はありません。

Reduxdoesは、非同期のものを処理するいくつかの代替方法を提供しますが、あまりにも多くのコードを繰り返していることに気付いた場合にのみ使用してください。この問題がない限り、言語が提供するものを使用して、最も簡単な解決策を探してください。

非同期コードのインライン記述

これが最も簡単な方法です。ここにはReduxに固有のものはありません。

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

同様に、接続されたコンポーネント内から:

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

唯一の違いは、接続されたコンポーネントでは通常、ストア自体にはアクセスできないが、dispatch()または特定のアクション作成者のいずれかを小道具として注入することです。しかし、これは私たちにとって何の違いももたらしません。

異なるコンポーネントから同じアクションをディスパッチするときにタイプミスをしたくない場合は、アクションオブジェクトをインラインでディスパッチするのではなく、アクションクリエーターを抽出することができます。

// actions.js
export function showNotification(text) {
  return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
  return { type: 'HIDE_NOTIFICATION' }
}

// component.js
import { showNotification, hideNotification } from '../actions'

this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => {
  this.props.dispatch(hideNotification())
}, 5000)

または、以前にconnect()でバインドしている場合:

this.props.showNotification('You just logged in.')
setTimeout(() => {
  this.props.hideNotification()
}, 5000)

これまでのところ、ミドルウェアやその他の高度な概念は使用していません。

非同期アクションクリエーターの抽出

上記のアプローチは単純なケースではうまく機能しますが、いくつかの問題があることがわかるかもしれません。

  • 通知を表示する場所にこのロジックを強制的に複製します。
  • 通知にはIDがないため、2つの通知を十分に速く表示すると競合状態になります。最初のタイムアウトが終了すると、HIDE_NOTIFICATIONをディスパッチし、タイムアウト後よりも早く誤って2番目の通知を非表示にします。

これらの問題を解決するには、タイムアウトロジックを集中化し、これら2つのアクションをディスパッチする関数を抽出する必要があります。次のようになります。

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  // Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION
  // for the notification that is not currently visible.
  // Alternatively, we could store the timeout ID and call
  // clearTimeout(), but we’d still want to do it in a single place.
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

コンポーネントは、このロジックを複製したり、異なる通知で競合状態になったりすることなく、showNotificationWithTimeoutを使用できるようになりました。

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

showNotificationWithTimeout()dispatchを最初の引数として受け入れるのはなぜですか?ストアにアクションをディスパッチする必要があるためです。通常、コンポーネントはdispatchにアクセスできますが、外部関数にディスパッチを制御させたいので、ディスパッチを制御する必要があります。

あるモジュールからエクスポートされたシングルトンストアがある場合は、それをインポートし、代わりにdispatchを直接インポートできます。

// store.js
export default createStore(reducer)

// actions.js
import store from './store'

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  const id = nextNotificationId++
  store.dispatch(showNotification(id, text))

  setTimeout(() => {
    store.dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout('You just logged in.')

// otherComponent.js
showNotificationWithTimeout('You just logged out.')    

これは簡単に見えますが、このアプローチはお勧めしません。嫌いな主な理由は、storeを強制的にシングルトンにするためです。これにより、 サーバーレンダリング の実装が非常に難しくなります。サーバーでは、各ユーザーが独自のストアを持ち、異なるユーザーが異なるプリロードされたデータを取得できるようにする必要があります。

また、シングルトンストアはテストを難しくします。アクション作成者は特定のモジュールからエクスポートされた特定の実際のストアを参照するため、アクション作成者をテストするときにストアをモックすることはできなくなりました。外部から状態をリセットすることもできません。

したがって、モジュールからシングルトンストアを技術的にエクスポートすることはできますが、お勧めしません。アプリがサーバーレンダリングを追加しないことが確実でない限り、これを行わないでください。

前のバージョンに戻る:

// actions.js

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

これにより、ロジックの重複に関する問題が解決され、競合状態から私たちを救います。

サンクミドルウェア

シンプルなアプリの場合、このアプローチで十分です。ミドルウェアに満足していれば心配する必要はありません。

ただし、大規模なアプリでは、特定の不便な点が見つかる場合があります。

たとえば、dispatchを渡す必要があるのは残念なようです。上記の方法でReduxアクションを非同期的にディスパッチするコンポーネントは、dispatchをプロップとして受け入れてさらに渡すことができるため、これは 個別のコンテナコンポーネントとプレゼンテーションコンポーネント を扱いにくくします。 connect()は実際にはアクションクリエーターではないため、アクションクリエーターをshowNotificationWithTimeout()にバインドすることはできません。 Reduxアクションを返しません。

さらに、どの関数がshowNotification()のような同期アクション作成者であり、どの関数がshowNotificationWithTimeout()のような非同期ヘルパーであるかを覚えるのは面倒です。それらを別々に使用し、互いに間違えないように注意する必要があります。

これは、ヘルパー関数にdispatchを提供するこのパターンを「正当化」し、Reduxがそのような非同期アクションクリエーターを特別なケースとして「参照」する方法を見つけるための動機でした。まったく異なる機能ではなく、通常のアクション作成者

引き続きご連絡いただき、アプリの問題として認識される場合は、 Redux Thunk ミドルウェアを使用してください。

要点では、ReduxサンクはReduxに実際に機能する特別な種類のアクションを認識するように教えています。

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)

// It still recognizes plain object actions
store.dispatch({ type: 'INCREMENT' })

// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) {
  // ... which themselves may dispatch many times
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })

  setTimeout(() => {
    // ... even asynchronously!
    dispatch({ type: 'DECREMENT' })
  }, 1000)
})

このミドルウェアが有効な場合、関数をディスパッチすると、Reduxサンクミドルウェアはdispatchを引数として与えます。また、そのようなアクションを「飲み込む」ので、レデューサーが奇妙な関数引数を受け取る心配はありません。レデューサーは単純なオブジェクトアクションのみを受け取ります-直接放出されるか、先ほど説明したように関数によって放出されます。

これはあまり有用ではありませんか?この特定の状況ではありません。ただし、showNotificationWithTimeout()を通常のReduxアクションクリエーターとして宣言できます。

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

関数が前のセクションで書いたものとほとんど同じであることに注意してください。ただし、dispatchを最初の引数として受け入れません。代わりに、最初の引数としてdispatchを受け入れる関数をreturnsします。

コンポーネントでどのように使用しますか?間違いなく、次のように書くことができます。

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)

非同期アクションクリエーターを呼び出して、dispatchのみを必要とする内部関数を取得し、dispatchを渡します。

しかし、これは元のバージョンよりもさらに厄介です!なぜそのようにしたのでしょうか?

前に言ったことがあります。 Reduxサンクミドルウェアが有効な場合、アクションオブジェクトの代わりに関数をディスパッチしようとすると、ミドルウェアはdispatchメソッド自体を最初の引数としてその関数を呼び出します

そのため、代わりにこれを行うことができます。

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

最後に、非同期アクション(実際には一連のアクション)をディスパッチすることは、単一のアクションをコンポーネントに同期的にディスパッチすることと変わりません。コンポーネントが何かが同期的に発生するか非同期的に発生するかを気にする必要がないため、これは良いことです。それを抽象化しました。

Reduxはそのような「特別な」アクションクリエーターを認識するように「教えた」ため( thunk アクションクリエーターと呼ぶ)、通常のアクションクリエーターを使用するあらゆる場所で使用できることに注意してください。たとえば、connect()でそれらを使用できます。

// actions.js

function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

サンクでの状態の読み取り

通常、レデューサーには次の状態を決定するためのビジネスロジックが含まれています。ただし、リデューサーはアクションがディスパッチされた後にのみ作動します。サンクアクションクリエーターに副作用(APIの呼び出しなど)があり、ある条件下でそれを防ぎたい場合はどうしますか?

サンクミドルウェアを使用せずに、コンポーネント内でこのチェックを行うだけです。

// component.js
if (this.props.areNotificationsEnabled) {
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}

ただし、アクションクリエーターを抽出するポイントは、この繰り返しのロジックを多くのコンポーネントに集中させることでした。幸いなことに、Redux ThunkはReduxストアの現在の状態をreadする方法を提供します。 dispatchに加えて、getStateを2番目の引数として、サンクアクションクリエーターから返す関数に渡します。これにより、サンクはストアの現在の状態を読み取ることができます。

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch, getState) {
    // Unlike in a regular action creator, we can exit early in a thunk
    // Redux doesn’t care about its return value (or lack of it)
    if (!getState().areNotificationsEnabled) {
      return
    }

    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

このパターンを乱用しないでください。キャッシュされたデータが利用可能な場合、APIコールを終了するのに適していますが、ビジネスロジックを構築するための非常に優れた基盤ではありません。 getState()を使用して条件付きで異なるアクションをディスパッチする場合のみ、代わりにビジネスロジックをリデューサーに配置することを検討してください。

次のステップ

サンクがどのように機能するかについての基本的な直感が得られたので、それらを使用するRedux async example を確認してください。

サンクがプロミスを返す多くの例を見つけるかもしれません。これは必須ではありませんが、非常に便利です。 Reduxは、サンクから何を返すかを気にしませんが、dispatch()からの戻り値を提供します。サンクからPromiseを返し、dispatch(someThunkReturningPromise()).then(...)を呼び出して完了するのを待つことができるのはこのためです。

複雑なサンクアクションクリエーターをいくつかの小さなサンクアクションクリエーターに分割することもできます。サンクによって提供されるdispatchメソッドは、サンク自体を受け入れることができるため、パターンを再帰的に適用できます。繰り返しますが、これはPromisesで最適に機能します。これは、その上に非同期制御フローを実装できるためです。

一部のアプリでは、非同期制御フローの要件が複雑すぎてサンクで表現できない場合があります。たとえば、失敗したリクエストの再試行、トークンを使用した再認証フロー、または段階的なオンボーディングは、この方法で記述した場合、冗長でエラーが発生しやすくなります。この場合、 Redux SagaRedux Loop などのより高度な非同期制御フローソリューションをご覧ください。それらを評価し、ニーズに関連する例を比較し、最も好きなものを選択します。

最後に、必要なもの(サンクを含む)を使用しないでください。要件に応じて、ソリューションは次のようにシンプルに見えることを忘れないでください

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

なぜそうするのかわからない限り、汗をかかないでください。

2407
Dan Abramov

Redux-sagaを使う

Dan Abramovが言ったように、非同期コードをもっと高度に制御したいのなら、 redux-saga を見てください。

この回答は単純な例です。なぜあなたのアプリケーションにredux-sagaが役に立つのかについてのより良い説明が欲しいなら、この他の回答 をチェックしてください。

一般的なアイディアは、同期コードのように見える非同期コードを簡単に書くことを可能にするES6ジェネレータインタプリタをRedux-sagaが提供するということです(Redux-sagaでは無限のwhileループを見つけることが多い理由です)。どういうわけか、Redux-sagaはJavascriptの中で直接独自の言語を構築しています。あなたはジェネレータの基本的な理解が必要であるが、Redux-sagaによって提供される言語も理解するので、Redux-sagaは最初は少し学ぶのが難しいと感じるかもしれません。

ここで、redux-sagaの上に構築した通知システムについて説明します。この例は現在本番稼働中です。

高度な通知システムの仕様

  • 表示する通知をリクエストできます
  • あなたは隠すために通知を要求することができます
  • 通知は4秒以上表示されてはいけません
  • 複数の通知を同時に表示することができます
  • 同時に表示できる通知は3つまでです。
  • すでに3つの通知が表示されているときに通知が要求された場合は、それをキューに入れるか延期します。

結果

私の制作アプリのスクリーンショット Stample.co

toasts

コード

ここでは通知をtoastと命名しましたが、これは命名の詳細です。

function* toastSaga() {

    // Some config constants
    const MaxToasts = 3;
    const ToastDisplayTime = 4000;


    // Local generator state: you can put this state in Redux store
    // if it's really important to you, in my case it's not really
    let pendingToasts = []; // A queue of toasts waiting to be displayed
    let activeToasts = []; // Toasts currently displayed


    // Trigger the display of a toast for 4 seconds
    function* displayToast(toast) {
        if ( activeToasts.length >= MaxToasts ) {
            throw new Error("can't display more than " + MaxToasts + " at the same time");
        }
        activeToasts = [...activeToasts,toast]; // Add to active toasts
        yield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)
        yield call(delay,ToastDisplayTime); // Wait 4 seconds
        yield put(events.toastHidden(toast)); // Hide the toast
        activeToasts = _.without(activeToasts,toast); // Remove from active toasts
    }

    // Everytime we receive a toast display request, we put that request in the queue
    function* toastRequestsWatcher() {
        while ( true ) {
            // Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatched
            const event = yield take(Names.TOAST_DISPLAY_REQUESTED);
            const newToast = event.data.toastData;
            pendingToasts = [...pendingToasts,newToast];
        }
    }


    // We try to read the queued toasts periodically and display a toast if it's a good time to do so...
    function* toastScheduler() {
        while ( true ) {
            const canDisplayToast = activeToasts.length < MaxToasts && pendingToasts.length > 0;
            if ( canDisplayToast ) {
                // We display the first pending toast of the queue
                const [firstToast,...remainingToasts] = pendingToasts;
                pendingToasts = remainingToasts;
                // Fork means we are creating a subprocess that will handle the display of a single toast
                yield fork(displayToast,firstToast);
                // Add little delay so that 2 concurrent toast requests aren't display at the same time
                yield call(delay,300);
            }
            else {
                yield call(delay,50);
            }
        }
    }

    // This toast saga is a composition of 2 smaller "sub-sagas" (we could also have used fork/spawn effects here, the difference is quite subtile: it depends if you want toastSaga to block)
    yield [
        call(toastRequestsWatcher),
        call(toastScheduler)
    ]
}

そして減力剤:

const reducer = (state = [],event) => {
    switch (event.name) {
        case Names.TOAST_DISPLAYED:
            return [...state,event.data.toastData];
        case Names.TOAST_HIDDEN:
            return _.without(state,event.data.toastData);
        default:
            return state;
    }
};

使用法

TOAST_DISPLAY_REQUESTEDイベントを単に送出することができます。 4つのリクエストをディスパッチした場合、3つの通知のみが表示され、最初の通知が消えると4番目の通知が少し遅れて表示されます。

JSXからTOAST_DISPLAY_REQUESTEDをディスパッチすることは特にお勧めしません。既存のアプリイベントをリッスンする別のサガを追加してから、TOAST_DISPLAY_REQUESTED:通知をトリガーするコンポーネントを通知システムに密接に結合する必要はありません。

結論

私のコードは完璧ではありませんが、数ヶ月間0のバグで本番稼働します。 Redux-sagaと発電機は最初は少し難しいですが、あなたがそれらを理解すれば、この種のシステムは構築するのがとても簡単です。

次のように、もっと複雑なルールを実装するのは非常に簡単です。

  • あまりにも多くの通知が「キューに入れられる」とき、キューサイズがより速く減少することができるように各通知のためのより少ない表示時間を与えます。
  • ウィンドウサイズの変更を検出し、それに応じて表示される通知の最大数を変更します(たとえば、desktop = 3、phone portrait = 2、phone landscape = 1)。

正直なところ、この種のものをさんくで正しく実装してくれて幸運です。

redux-observable とまったく同じ種類のことができることに注意してください。これはredux-sagaと非常によく似ています。それはほとんど同じで、ジェネレータとRxJSの間の好みの問題です。

165

SAMパターン を見てみることをお勧めします。

SAMパターンでは、モデルが更新されると「通知が5秒後に自動的に消える」などの(自動)アクションがトリガされる「next-action-predicate」を含めることが推奨されています(SAM model〜reducer state + store)。

モデルの「制御状態」は、どのアクションが次のアクション述語によって有効にされるか、および/または自動的に実行されるかを「制御する」ため、パターンは、シーケンスアクションとモデル突然変異を一度に1つずつ推奨します。システムがアクションを処理する前にどのような状態になるのか、したがって、次に予想されるアクションが許可されるかどうか(単純に)予測することはできません。

だから、例えば、コード、

export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

hideNotificationアクションをディスパッチできるという事実は、値「showNotication:true」を正常に受け入れたモデルに依存するため、SAMでは許可されません。モデルがそれを受け入れないようにするモデルの他の部分が存在する可能性があるため、hideNotificationアクションをトリガーする理由はありません。

ストアが更新され、モデルの新しい制御状態がわかるようになったら、適切なnext-action述語を実装することを強くお勧めします。それがあなたが探している振る舞いを実装するための最も安全な方法です。

ご希望の場合は、Gitterに参加してください。 SAM入門ガイドもあります

19

redux-thunk でこれを実行できます。 redux文書に/ - ガイドがあります setTimeoutのような非同期動作のための/。

19
Fatih Erikli

サンプルプロジェクトのあるリポジトリ

現在4つのサンプルプロジェクトがあります。

  1. 非同期コードをインラインで書く
  2. 非同期アクション作成者の抽出
  3. Redux Thunkを使う
  4. Redux Sagaを使う

受け入れられた答えは素晴らしいです。

しかし、足りないものがあります。

  1. 実行可能なサンプルプロジェクトはありません。コードスニペットがいくつかあります。
  2. 以下のような他の選択肢のサンプルコードはありません。
    1. Redux Saga

そこで、欠けているものを追加するために Hello Async リポジトリを作成しました。

  1. 実行可能なプロジェクト変更せずにダウンロードして実行できます。
  2. より多くの選択肢のためのサンプルコードを提供してください:

レダックス佐賀

承認された回答では、Async Code Inline、Async Action Generator、およびRedux Thunkのサンプルコードスニペットがすでに提供されています。完全を期すために、Redux Sagaのコードスニペットを提供します。

// actions.js

export const showNotification = (id, text) => {
  return { type: 'SHOW_NOTIFICATION', id, text }
}

export const hideNotification = (id) => {
  return { type: 'HIDE_NOTIFICATION', id }
}

export const showNotificationWithTimeout = (text) => {
  return { type: 'SHOW_NOTIFICATION_WITH_TIMEOUT', text }
}

行動は単純で純粋です。

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

コンポーネントに関して特別なことは何もありません。

// sagas.js

import { takeEvery, delay } from 'redux-saga'
import { put } from 'redux-saga/effects'
import { showNotification, hideNotification } from './actions'

// Worker saga
let nextNotificationId = 0
function* showNotificationWithTimeout (action) {
  const id = nextNotificationId++
  yield put(showNotification(id, action.text))
  yield delay(5000)
  yield put(hideNotification(id))
}

// Watcher saga, will invoke worker saga above upon action 'SHOW_NOTIFICATION_WITH_TIMEOUT'
function* notificationSaga () {
  yield takeEvery('SHOW_NOTIFICATION_WITH_TIMEOUT', showNotificationWithTimeout)
}

export default notificationSaga

Sagasは ES6ジェネレータ に基づいています

// index.js

import createSagaMiddleware from 'redux-saga'
import saga from './sagas'

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(saga)

Redux Thunkと比較して

長所

  • あなたはコールバック地獄に陥ることはありません。
  • 非同期フローを簡単にテストできます。
  • あなたの行動は純粋なままです。

短所

  • 比較的新しいES6ジェネレーターに依存しています。

runnableプロジェクトを参照してください 上記のコードスニペットがあなたの質問すべてに答えない場合。

18
Tyler Long

さまざまな人気のあるアプローチ(アクションクリエーター、サンク、サガ、エピック、エフェクト、カスタムミドルウェア)を試した後、まだ改善の余地があると感じたので、このブログ記事に私の旅を記録しました どこに置くかReact/Reduxアプリケーションでのビジネスロジック?

ここでの議論と同じように、私はさまざまなアプローチを対比して比較しようとしました。最終的には、叙事詩、サガ、カスタムミドルウェアからインスピレーションを得た新しいライブラリ redux-logic を導入することになりました。

非同期IOを実行する方法を提供するだけでなく、検証、検証、承認するアクションをインターセプトできます。

いくつかの一般的な機能は、デバウンス、スロットリング、キャンセル、および最新のリクエスト(takeLatest)からの応答のみを使用するように宣言できます。 redux-logicは、この機能を提供するコードをラップします。

これにより、コアビジネスロジックを自由に実装できます。必要でない限り、オブザーバブルまたはジェネレーターを使用する必要はありません。関数とコールバック、Promise、非同期関数(async/await)などを使用します。

簡単な5秒通知を行うためのコードは次のようになります。

const notificationHide = createLogic({
  // the action type that will trigger this logic
  type: 'NOTIFICATION_DISPLAY',
  
  // your business logic can be applied in several
  // execution hooks: validate, transform, process
  // We are defining our code in the process hook below
  // so it runs after the action hit reducers, hide 5s later
  process({ getState, action }, dispatch) {
    setTimeout(() => {
      dispatch({ type: 'NOTIFICATION_CLEAR' });
    }, 5000);
  }
});
    

私のリポジトリには、セバスチャン・ローバーが表示をN個のアイテムに制限し、キューに入れられたアイテムを回転させることができる場所と同様に機能する、より高度な通知の例があります。 redux-logic通知の例

さまざまな redux-logic jsfiddleの実例と完全な例 があります。私はドキュメントとサンプルの作業を続けています。

ご意見をお聞かせください。

17
Jeff Barczewski

私はこの質問は少し古いことを理解していますが、 redux-observable akaを使った別の解決策を紹介するつもりです。エピック。

公式文書を引用する:

レデックス観測可能とは何ですか?

Redux用のRxJS 5ベースのミドルウェア。非同期アクションを作成してキャンセルすると、副作用などが発生します。

叙事詩はredux-observableのコアプリミティブです。

これは、一連のアクションを受け取り、一連のアクションを返す関数です。アクションイン、アクションアウト.

多かれ少なかれ、Streamを介してアクションを受け取り、その後新しいタイムアウトのアクションを返す関数を作成することができます(タイムアウト、遅延、間隔、リクエストなどの一般的な副作用を使用して)。

コードを投稿して、それについてもう少し説明しましょう。

store.js

import {createStore, applyMiddleware} from 'redux'
import {createEpicMiddleware} from 'redux-observable'
import {Observable} from 'rxjs'
const NEW_NOTIFICATION = 'NEW_NOTIFICATION'
const QUIT_NOTIFICATION = 'QUIT_NOTIFICATION'
const NOTIFICATION_TIMEOUT = 2000

const initialState = ''
const rootReducer = (state = initialState, action) => {
  const {type, message} = action
  console.log(type)
  switch(type) {
    case NEW_NOTIFICATION:
      return message
    break
    case QUIT_NOTIFICATION:
      return initialState
    break
  }

  return state
}

const rootEpic = (action$) => {
  const incoming = action$.ofType(NEW_NOTIFICATION)
  const outgoing = incoming.switchMap((action) => {
    return Observable.of(quitNotification())
      .delay(NOTIFICATION_TIMEOUT)
      //.takeUntil(action$.ofType(NEW_NOTIFICATION))
  });

  return outgoing;
}

export function newNotification(message) {
  return ({type: NEW_NOTIFICATION, message})
}
export function quitNotification(message) {
  return ({type: QUIT_NOTIFICATION, message});
}

export const configureStore = () => createStore(
  rootReducer,
  applyMiddleware(createEpicMiddleware(rootEpic))
)

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {configureStore} from './store.js'
import {Provider} from 'react-redux'

const store = configureStore()

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

App.js

import React, { Component } from 'react';
import {connect} from 'react-redux'
import {newNotification} from './store.js'

class App extends Component {

  render() {
    return (
      <div className="App">
        {this.props.notificationExistance ? (<p>{this.props.notificationMessage}</p>) : ''}
        <button onClick={this.props.onNotificationRequest}>Click!</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    notificationExistance : state.length > 0,
    notificationMessage : state
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onNotificationRequest: () => dispatch(newNotification(new Date().toDateString()))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

この問題を解決するための重要なコードは、見てわかるほど簡単です。他の答えと異なるのは、関数rootEpicだけです。

ポイント1。サガと同様に、アクションのストリームを受け取り、アクションのストリームを返すトップレベルの関数を取得するために叙事詩を組み合わせる必要があるので、ミドルウェアファクトリ createEpicMiddleware と一緒に使うことができます。 。私たちの場合、必要なものは1つだけなので、 rootEpic しかありませんので、何も組み合わせる必要はありませんが、事実を知っておくのは良いことです。

Point2。副作用ロジックを考慮している私たちの rootEpic は、すばらしい5行のコードしか必要としません。ほとんど宣言的であるという事実を含めて!

Point 3.行ごとrootEpicの説明(コメント内)

const rootEpic = (action$) => {
  // sets the incoming constant as a stream 
  // of actions with  type NEW_NOTIFICATION
  const incoming = action$.ofType(NEW_NOTIFICATION)
  // Merges the "incoming" stream with the stream resulting for each call
  // This functionality is similar to flatMap (or Promise.all in some way)
  // It creates a new stream with the values of incoming and 
  // the resulting values of the stream generated by the function passed
  // but it stops the merge when incoming gets a new value SO!,
  // in result: no quitNotification action is set in the resulting stream
  // in case there is a new alert
  const outgoing = incoming.switchMap((action) => {
    // creates of observable with the value passed 
    // (a stream with only one node)
    return Observable.of(quitNotification())
      // it waits before sending the nodes 
      // from the Observable.of(...) statement
      .delay(NOTIFICATION_TIMEOUT)
  });
  // we return the resulting stream
  return outgoing;
}

私はそれが役立つことを願っています!

7
cnexans

なぜそれはそんなに難しいのですか?それは単なるUIロジックです。通知データを設定するには、専用のアクションを使用します。

dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })

それを表示するための専用コンポーネント

const Notifications = ({ notificationData }) => {
    if(notificationData.expire > this.state.currentTime) {
      return <div>{notificationData.message}</div>
    } else return null;
}

この場合の質問は、「どのように古い状態をクリーンアップしますか?」、「コンポーネントに時間が変わったことを通知する方法」です。

コンポーネントからsetTimeoutにディスパッチされるTIMEOUTアクションを実装することができます。

新しい通知が表示されるたびにそれをきれいにしても大丈夫です。

とにかく、どこかにsetTimeoutがあるはずですよね?コンポーネントでやらないのはなぜ

setTimeout(() => this.setState({ currentTime: +new Date()}), 
           this.props.notificationData.expire-(+new Date()) )

動機は、「通知フェードアウト」機能が本当にUIの問題であるということです。そのため、ビジネスロジックのテストが簡単になります。

実装方法をテストしても意味がありません。通知がタイムアウトするタイミングを確認することだけが意味があります。したがって、スタブするコードが少なくなり、テストが速くなり、コードがきれいになります。

5
Vanuan

選択的なアクションでタイムアウト処理をしたい場合は、 ミドルウェア の方法を試すことができます。私は約束ベースのアクションを選択的に処理するために同様の問題に直面しました、そしてこの解決策はより柔軟でした。

あなたのアクション作成者がこのようになっているとしましょう。

//action creator
buildAction = (actionData) => ({
    ...actionData,
    timeout: 500
})

上記のアクションでtimeoutは複数の値を保持できます

  • ミリ秒単位の数値 - 特定のタイムアウト期間
  • true - 一定のタイムアウト期間(ミドルウェアで処理)
  • 未定義 - 即時ディスパッチ用

ミドルウェアの実装は次のようになります。

//timeoutMiddleware.js
const timeoutMiddleware = store => next => action => {

  //If your action doesn't have any timeout attribute, fallback to the default handler
  if(!action.timeout) {
    return next (action)
  }

  const defaultTimeoutDuration = 1000;
  const timeoutDuration = Number.isInteger(action.timeout) ? action.timeout || defaultTimeoutDuration;

//timeout here is called based on the duration defined in the action.
  setTimeout(() => {
    next (action)
  }, timeoutDuration)
}

これで、reduxを使用して、このミドルウェア層を介してすべてのアクションをルーティングできます。

createStore(reducer, applyMiddleware(timeoutMiddleware))

あなたはいくつかの同様の例を見つけることができます ここ

5
Yash

これを行うための適切な方法は、 Redux Thunk を使用することです。これは、Redux Thunkのドキュメントにあるように、Redux用の一般的なミドルウェアです。

「Redux Thunkミドルウェアを使用すると、アクションの代わりに関数を返すアクション作成者を作成できます。サンクは、アクションのディスパッチを遅らせるため、または特定の条件が満たされた場合にのみディスパッチするために使用できます。内部関数はストアメソッドを受け取りますパラメータとしてのdispatchとgetState "#:。

そのため、基本的に関数を返します。そして、あなたはあなたの派遣を遅らせるか、またはそれを条件状態にすることができます。

だから、このような何かがあなたのために仕事をするつもりです:

import ReduxThunk from 'redux-thunk';

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 5000);
  };
}
3
Alireza

簡単です。 trim-redux packageを使用して、componentDidMountまたは他の場所に次のように記述し、componentWillUnmountに配置します。

componentDidMount() {
  this.tm = setTimeout(function() {
    setStore({ age: 20 });
  }, 3000);
}

componentWillUnmount() {
  clearTimeout(this.tm);
}

Redux自体はかなり冗長なライブラリです、そしてそのようなもののためにあなたはdispatch関数を与えるであろう Redux-thunk のようなものを使用しなければならないでしょう。 。

冗長性や構成可能性などの問題に対処するためのライブラリ を作成しました。この例は次のようになります。

import { createTile, createSyncTile } from 'redux-tiles';
import { sleep } from 'delounce';

const notifications = createSyncTile({
  type: ['ui', 'notifications'],
  fn: ({ params }) => params.data,
  // to have only one tile for all notifications
  nesting: ({ type }) => [type],
});

const notificationsManager = createTile({
  type: ['ui', 'notificationManager'],
  fn: ({ params, dispatch, actions }) => {
    dispatch(actions.ui.notifications({ type: params.type, data: params.data }));
    await sleep(params.timeout || 5000);
    dispatch(actions.ui.notifications({ type: params.type, data: null }));
    return { closed: true };
  },
  nesting: ({ type }) => [type],
});

そのため、非同期アクション内に通知を表示するための同期アクションを作成します。これは、バックグラウンドで情報を要求することも、後で通知が手動で閉じられたかどうかを確認することもできます。

1
Bloomca