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


891

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

私はsetTimeout別のアクションを使用したり返したりすることができず、オンラインでこれを行う方法を見つけることができません。ですから、アドバイスは大歓迎です。


30
redux-sagaサンクより良いものが欲しいなら、私の答えをチェックすることを忘れないでください。答えが遅いので、表示される前に長い間スクロールする必要があります。これがショートカットです:stackoverflow.com/a/38574266/82609
Sebastien Lorber

5
setTimeoutを実行するときはいつでも、componentWillUnMountライフサイクルメソッドでclearTimeoutを使用してタイマーをクリアすることを忘れないでください
Hemadri Dasari 2018

2
redux-sagaはクールですが、ジェネレーター関数からの型付き応答をサポートしていないようです。typescriptをreactと一緒に使用している場合は問題になるかもしれません。
キリスト教のラミレス

回答:


2617

図書館がすべてを行う方法を規定すべきだと考える罠に陥らないでください。JavaScriptでタイムアウトを使って何かを実行したい場合は、を使用する必要がありますsetTimeout。Reduxのアクションが異なる理由はありません。

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

非同期コードをインラインで書く

これは、最も簡単な方法です。そして、ここでは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.')    

これは単純に見えますが、このアプローチはお勧めしません。私たちがそれを嫌う主な理由は、ストアをシングルトンにすることを強制するためです。これにより、サーバーレンダリングの実装が非常に困難になります。。サーバーでは、各リクエストに独自のストアを設定して、異なるユーザーが異なるプリロードデータを取得できるようにします。

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

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

以前のバージョンに戻す:

// 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 Thunkは、実際に機能する特別な種類のアクションを認識するよう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 Thunkミドルウェアがそれ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ます。

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

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

非同期アクションの作成者を呼び出して、必要なだけの内部関数を取得してから、dispatch渡しdispatchます。

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

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

代わりにこれを行うことができます:

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

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

Reduxにそのような「特別な」アクションクリエーター(サンクアクションクリエーターと呼ぶ)を認識するように「教えた」ため、通常のアクションクリエーターを使用する場所ならどこでも使用できることに注意してください。たとえば、次のように使用できます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ストアの現在の状態を読み取ることができます。に加えてdispatchgetStateサンクアクションの作成者から返す関数の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 非同期の例を確認してください。

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

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

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

最後に、本当に必要なものがない場合は(サンクを含む)何も使用しないでください。要件によっては、ソリューションが次のように単純に見える場合があることに注意してください

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

なぜこれをしているのかわからない限り、汗をかいてはいけません。


27
非同期アクションは、一般的な問題に対するこのようなシンプルでエレガントなソリューションのように見えます。ミドルウェアを必要とせずに、それらをサポートするためのサポートがなぜないのでしょうか。そうすれば、この答えはもっと簡潔になるでしょう。
Phil Mander 2016

83
@PhilMander github.com/raisemarketplace/redux-loopgithub.com/yelouafi/redux-sagaのような多くの代替パターンがあるのでエレガントではありません。Reduxは低レベルのツールです。好みのスーパーセットを作成して、個別に配布できます。
Dan Abramov、2016

16
これについて説明してください:*ビジネスロジックをレデューサーに入れることを検討してください*これは、アクションをディスパッチする必要があることを意味します。次に、レデューサーで、状態に応じてディスパッチするアクションを決定しますか?私の質問は、他のアクションをレデューサーで直接ディスパッチするのですか、そうでない場合はどこからディスパッチするのですか?
froginvasion

25
この文は同期の場合にのみ適用されます。たとえば、もしあなたがif (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })多分書いているなら、現在の状態dispatch({ type: 'C', something: cond })に応じてaction.something、代わりにレデューサーのアクションを無視することを選択すべきです。
Dan Abramov

29
@DanAbramovあなたはこのために私の賛成を得ました「この問題がない限り、言語が提供するものを使用し、最も簡単な解決策に進んでください」。誰がそれを書いたのかを知って初めて!
Matt Lacey

189

Redux-sagaの使用

Dan Abramovが言ったように、非同期コードのより高度な制御が必要な場合は、redux-sagaをご覧ください。

この回答は簡単な例です。redux-sagaがアプリケーションに役立つ理由を詳しく説明したい場合は、この別の回答を確認してください

一般的な考え方は、Redux-sagaが、同期コードのように見える非同期コードを簡単に作成できるES6ジェネレーターインタープリターを提供しているということです(これがRedux-sagaで無限のwhileループを頻繁に見つける理由です)。どういうわけか、Redux-sagaはJavascript内で直接独自の言語を構築しています。ジェネレーターの基本的な理解が必要であるだけでなく、Redux-sagaが提供する言語も理解しているため、Redux-sagaは最初は少し難しいと感じるかもしれません。

ここで、私がredux-sagaの上に構築した通知システムについて説明します。この例は現在本番環境で実行されています。

高度な通知システム仕様

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

結果

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

乾杯

コード

ここでは通知にaという名前を付けました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番目のリクエストは少し遅れて表示されます。

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

結論

私のコードは完璧ではありませんが、何ヶ月もバグなしで運用環境で実行されます。Redux-sagaとジェネレーターは最初は少し難しいですが、いったん理解すれば、この種のシステムを構築するのはかなり簡単です。

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

  • 「キューに入れられる」通知が多すぎる場合は、各通知の表示時間を短くして、キューのサイズをより速く減らすことができます。
  • ウィンドウサイズの変更を検出し、それに応じて表示される通知の最大数を変更します(たとえば、デスクトップ= 3、電話の縦= 2、電話の横= 1)

正直なところ、サンクでこの種のものを適切に実装できるように頑張ってください。

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


18
このようなビジネスロジックに佐賀の副作用ライブラリを使用することにこれ以上同意できないので、質問があったときにあなたの答えが以前に来たことを願っています。レデューサーとアクションクリエーターは、状態遷移用です。ワークフローは、状態遷移関数と同じではありません。ワークフローは遷移をステップスルーしますが、それ自体は遷移ではありません。Redux + Reactだけではこれがありません-これがRedux Sagaが非常に便利な理由です。
Atticus

4
おかげで、私はこれらの理由のために再来-佐賀が人気にするために最善を尽くししよう:)あまりにも少数の人々は現在、Reduxの-サガはサンクのためだけの交換で、Reduxの-サガが複雑と切り離さワークフローを実現する方法が表示されないと思う
セバスチャン・ローバー

1
丁度。アクションとリデューサーはすべて状態マシンの一部です。複雑なワークフローの場合、ステートマシン自体の一部ではない、ステートマシンを調整するために別のものが必要になることがあります。
Atticus

2
アクション:ペイロード/イベントから状態遷移。レデューサー:状態遷移関数。コンポーネント:状態を反映するユーザーインターフェイス。しかし、欠けている大きな要素が1つあります。どのトランジションを次に実行するかを決定する独自のロジックを持つすべてのトランジションのプロセスをどのように管理しますか?Redux Saga!
Atticus

2
@mrbrdo私の回答を注意深く読むと、通知タイムアウトが実際に処理されていることがわかりますyield call(delay,timeoutValue);。これは同じAPIではありませんが、同じ効果があります
Sebastien Lorber

25

サンプルプロジェクトを含むリポジトリ

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

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

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

ただし、不足しているものがあります。

  1. 実行可能なサンプルプロジェクトはなく、いくつかのコードスニペットのみ。
  2. 次のような他の代替のサンプルコードはありません。
    1. Redux Saga

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

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

Redux Saga

承認された回答には、非同期コードインライン、非同期アクションジェネレーター、およびReduxサンクのサンプルコードスニペットがすでに含まれています。完全を期すために、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ジェネレーターに依存します。

上記のコードスニペットですべての質問に答えられない場合は、実行可能なプロジェクトを参照してください。


23

これはredux-thunkで実行できます。setTimeoutなどの非同期アクションについては、reduxドキュメントにガイドがあります。


簡単なフォローアップの質問です。ミドルウェアを使用applyMiddleware(ReduxPromise, thunk)(createStore)する場合、これでいくつかのミドルウェア(コマ区切り?)を追加できます。サンクが機能していないようです。
Ilja 2016

1
@Iljaこれは動作するはずです:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier

22

SAMパターンも確認することをお勧めします

SAMパターンは、「next-action-predicate」を含めることを提唱しており、モデルが更新されると(「5秒後に自動的に通知が自動的に消える」など)アクションがトリガーされます(SAMモデル〜レデューサーの状態+ストア)。

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

したがって、たとえばコード

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

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

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

ストアの更新後に適切な次のアクションの述語を実装し、モデルの新しい制御状態を把握できるようにすることを強くお勧めします。これが、探している動作を実装する最も安全な方法です。

よろしければ、Gitterに参加してください。ここには、SAM入門ガイドもあります


私はこれまで表面を引っかいただけでしたが、SAMパターンにはすでに興奮しています。V = S( vm( M.present( A(data) ) ), nap(M))ただ美しいです。あなたの考えと経験を共有してくれてありがとう。深く掘り下げます。

@ftor、ありがとう!初めて書いたときも同じ気持ちでした。私はSAMを実稼働で1年近く使用していますが、SAMを実装するためのライブラリーが必要だと感じたときは考えられません(vdomでも、いつ使用できるかはわかります)。たった1行のコードで終わりです。SAMは同型のコードを生成します。非同期呼び出しを処理する方法に曖昧さはありません...私が何をしているのかを考えることはできませんか?
メタプログラマ2017年

SAMは真のソフトウェアエンジニアリングパターンです(それを使用してAlexa SDKを作成しただけです)。これはTLA +に基づいており、その驚くべき作業の力をすべての開発者にもたらすことを試みます。SAMは、(かなり)誰もが何十年も使用している3つの近似値を修正します。-アクションはアプリケーションの状態を操作できます-割り当てはミューテーションと同等です-プログラミングステップの正確な定義はありません(例:a = b * caステップ) 、1 / b、cを読み取る2 / b * cを計算する、3 /結果をaに割り当てる3つの異なるステップ?
メタプログラマー

20

さまざまな人気のあるアプローチ(アクションクリエーター、サンク、サガ、エピック、エフェクト、カスタムミドルウェア)を試した後でも、まだ改善の余地があると感じたので、このブログ記事に自分の旅を記録しました。 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アイテムに制限し、キューに入れられたアイテムを順番に表示できるSebastian Lorberの説明と同様に機能します。redux-logic通知の例

さまざまなredux-logic jsfiddleライブの例と完全な例がありますます。私はドキュメントと例に取り組んでいます。

私はあなたのフィードバックを聞いてみたいです。


私はあなたのライブラリが好きかどうかはわかりませんが、あなたの記事は好きです!よくやったね!あなたは他の人の時間を節約するのに十分な仕事をしました。
タイラーロング

2
:私はここReduxのロジックのためのサンプルプロジェクトを作成しgithub.com/tylerlong/hello-async/tree/master/redux-logic 私はそれがソフトウェアのよくデザイン作品だと思うし、私は他に比べてすべての主要な欠点が表示されません代替。
タイラーロング

9

この質問は少し古いことを理解していますが、redux-observable aka を使用した別のソリューションを紹介します。大作。

公式ドキュメントを引用:

再観測可能とは何ですか?

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

エピックは、reduxで観測可能なコアプリミティブです。

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

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

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

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. sagasと同様に、アクションのストリームを受け取ってアクションのストリームを返すトップレベルの関数を取得するには、エピックを組み合わせて、ミドルウェアファクトリcreateEpicMiddlewareで使用できるようにする必要があります。私たちの場合、必要なのは1つだけなので、rootEpicだけを持っているので、何も組み合わせる必要はありませんが、事実を知るのは良いことです。

ポイント2.私たちのrootEpic副作用ロジックを処理するは約5行のコードしか必要ないので、すばらしいです。かなり宣言的であるという事実を含みます!

ポイント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;
}

お役に立てば幸いです。


特定のAPIメソッドがここで何をしているのか説明していただけswitchMapますか?
Dmitri Zaitsev

1
Windows上のReact Nativeアプリではredux-observableを使用しています。これは、複雑で高度に非同期の問題に対するエレガントな実装ソリューションであり、GitterチャネルとGitHubの問題を介して素晴らしいサポートを提供します。もちろん、追加の複雑さの層は、解決しようとしている正確な問題に到達した場合にのみ価値があります。
マット

8

なぜそんなに難しいのですか?それは単なる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の懸念事項であるということです。したがって、ビジネスロジックのテストが簡略化されます。

それがどのように実装されているかをテストすることは意味をなさないようです。通知がタイムアウトするタイミングを確認するだけで意味があります。したがって、スタブするコードが減り、テストが速くなり、コードがすっきりします。


1
これが一番の答えになるはずです。
mmla

6

選択的なアクションのタイムアウト処理が必要な場合は、ミドルウェアを試すことができますアプローチを。私はプロミスベースのアクションを選択的に処理するために同様の問題に直面し、このソリューションはより柔軟でした。

アクションクリエーターが次のようになっているとします。

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

タイムアウトは上記のアクションで複数の値を保持できます

  • ミリ秒単位の数値-特定のタイムアウト期間
  • 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

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

「Redux Thunkミドルウェアを使用すると、アクションの代わりに関数を返すアクションクリエーターを作成できます。このサンクを使用して、アクションのディスパッチを遅らせたり、特定の条件が満たされた場合にのみディスパッチしたりできます。内部関数はストアメソッドを受け取りますパラメータとしてのディスパッチと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);
  };
}

4

簡単です。trim-reduxパッケージを使用して、componentDidMount以下のようにこのように記述し、でkillしcomponentWillUnmountます。

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

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

3

Redux自体はかなり冗長なライブラリであり、そのようなものについては、関数を提供するRedux-thunkのようなものを使用dispatchする必要があるため、数秒後に通知のクローズをディスパッチできます。

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

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],
});

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.