アプリケーションの通知状態を更新するアクションがあります。通常、この通知はエラーまたは何らかの情報です。次に、通知状態を最初の状態に戻す 5 秒後に別のアクションをディスパッチする必要があるため、通知はありません。これの背後にある主な理由は、通知が 5 秒後に自動的に消える機能を提供することです。
別のアクションを使用して返すことに運がsetTimeout
なく、これがオンラインで行われる方法を見つけることができません。ですから、どんなアドバイスでも大歓迎です。
アプリケーションの通知状態を更新するアクションがあります。通常、この通知はエラーまたは何らかの情報です。次に、通知状態を最初の状態に戻す 5 秒後に別のアクションをディスパッチする必要があるため、通知はありません。これの背後にある主な理由は、通知が 5 秒後に自動的に消える機能を提供することです。
別のアクションを使用して返すことに運がsetTimeout
なく、これがオンラインで行われる方法を見つけることができません。ですから、どんなアドバイスでも大歓迎です。
図書館がすべてを行う方法を規定するべきであるという考えのわなに陥らないでください。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)
これまでのところ、ミドルウェアやその他の高度な概念は使用していません。
上記のアプローチは単純なケースでは問題なく機能しますが、いくつかの問題があることに気付くかもしれません。
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 を singleton に強制するためです。これにより、サーバー レンダリングの実装が非常に難しくなります。サーバーでは、各リクエストに独自のストアを持たせて、さまざまなユーザーがさまざまなプリロード データを取得できるようにします。
また、シングルトン ストアはテストを難しくします。アクションクリエーターは特定のモジュールからエクスポートされた特定の実際のストアを参照するため、アクションクリエーターをテストするときにストアをモックすることはできなくなりました。外部からその状態をリセットすることさえできません。
そのため、技術的にはモジュールからシングルトン ストアをエクスポートできますが、お勧めしません。アプリがサーバー レンダリングを追加しないことが確実でない限り、これを行わないでください。
以前のバージョンに戻す:
// 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 アクションを非同期にディスパッチするコンポーネントは、さらにそれを渡すことができるように prop として受け入れる必要があるため、これによりコンテナーとプレゼンテーション コンポーネントを分離するのが難しくなります。は実際にはアクション クリエーターではないため、dispatch
アクション クリエーターをバインドすることはできません。Redux アクションを返しません。connect()
showNotificationWithTimeout()
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 ストアの現在の状態を読み取る方法を提供します。に加えて、サンク アクション作成者から返される関数の 2 番目の引数としてdispatch
も渡します。getState
これにより、サンクはストアの現在の状態を読み取ることができます。
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 の例を確認してください。
サンクが Promise を返す多くの例を見つけることができます。これは必須ではありませんが、非常に便利です。Redux はサンクから何を返すかは気にしませんが、 からの戻り値を返しますdispatch()
。これが、サンクから Promise を返し、 を呼び出して完了するのを待つことができる理由ですdispatch(someThunkReturningPromise()).then(...)
。
複雑なサンク アクション クリエーターをいくつかの小さなサンク アクション クリエーターに分割することもできます。thunksdispatch
によって提供されるメソッドは、thunks 自体を受け入れることができるため、パターンを再帰的に適用できます。繰り返しますが、これは Promises で最もうまく機能します。これは、その上に非同期制御フローを実装できるためです。
一部のアプリでは、非同期制御フローの要件が複雑すぎてサンクで表現できない場合があります。たとえば、失敗したリクエストの再試行、トークンを使用した再認証フロー、または段階的なオンボーディングは、このように記述すると冗長になりすぎてエラーが発生しやすくなる可能性があります。この場合、Redux SagaやRedux Loopなどのより高度な非同期制御フロー ソリューションを検討することをお勧めします。それらを評価し、ニーズに関連する例を比較して、最も気に入ったものを選択してください。
最後に、本当に必要がない場合は、何も (サンクを含め) 使用しないでください。要件によっては、ソリューションが次のように単純に見える場合があることを覚えておいてください。
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
なぜこれを行っているのかわからない場合は、気にしないでください。
Dan Abramov が言ったように、非同期コードをより高度に制御したい場合は、redux-sagaを参照してください。
この回答は単純な例です。アプリケーションで redux-saga が役立つ理由についてより良い説明が必要な場合は、この他の回答を確認してください。
一般的な考え方は、Redux-saga が ES6 ジェネレーター インタープリターを提供し、同期コードのように見える非同期コードを簡単に記述できるようにすることです (これが、Redux-saga で無限の while ループがよく見られる理由です)。どういうわけか、Redux-saga は Javascript 内で直接独自の言語を構築しています。Redux-saga は、ジェネレーターの基本的な理解が必要なだけでなく、Redux-saga が提供する言語も理解する必要があるため、最初は少し難しく感じるかもしれません。
ここでは、redux-saga の上に構築した通知システムについて説明します。この例は現在、本番環境で実行されています。
制作アプリ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 つの通知のみが表示され、1 つ目の通知が消えると、4 つ目の通知が少し遅れて表示されます。
TOAST_DISPLAY_REQUESTED
JSX からのディスパッチは特にお勧めしません。既存のアプリ イベントをリッスンする別のサガを追加してからTOAST_DISPLAY_REQUESTED
、通知をトリガーするコンポーネントをディスパッチすることをお勧めします。通知システムに密接に結合されている必要はありません。
私のコードは完璧ではありませんが、何ヶ月もバグなしで本番環境で実行できます。Redux-saga とジェネレーターは最初は少し難しいですが、一度理解すれば、この種のシステムを構築するのは非常に簡単です。
次のような、より複雑なルールを実装することも非常に簡単です。
正直なところ、サンクを使用してこの種のものを適切に実装できたことを幸運に思います。
redux-saga と非常によく似たredux-observableでもまったく同じことができることに注意してください。それはほとんど同じで、ジェネレーターと RxJS の間の好みの問題です。
現在、4 つのサンプル プロジェクトがあります。
受け入れられた答えは素晴らしいです。
しかし、何かが欠けています:
そこで、足りないものを追加するためにHello Asyncリポジトリを作成しました。
受け入れられた回答では、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 Generatorsに基づいています
// 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で行うことができます。setTimeout のような非同期アクションのredux ドキュメントにガイドがあります。
さまざまな一般的なアプローチ (アクション クリエーター、サンク、サガ、エピック、エフェクト、カスタム ミドルウェア) を試した後、まだ改善の余地があると感じたので、このブログ記事「ビジネス ロジックをどこに配置すればよいか」で旅を記録しました。 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);
}
});
私のレポには、Sebastian Lorber が説明したように、表示を N 個のアイテムに制限し、キューに入っているアイテムをローテーションすることができる、より高度な通知の例があります。redux-logic 通知の例
さまざまなredux-logic jsfiddle のライブ サンプルと完全なサンプルがあります。私はドキュメントと例に取り組み続けています。
フィードバックをお待ちしております。
選択的なアクションでタイムアウト処理が必要な場合は、ミドルウェアアプローチを試すことができます。promise ベースのアクションを選択的に処理するために同様の問題に直面しましたが、このソリューションはより柔軟でした。
アクション作成者が次のようになっているとしましょう。
//action creator
buildAction = (actionData) => ({
...actionData,
timeout: 500
})
タイムアウトは、上記のアクションで複数の値を保持できます
ミドルウェアの実装は次のようになります。
//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))
ここで同様の例をいくつか見つけることができます