タイムアウト付きの Redux アクションをディスパッチするにはどうすればいいですか? 質問する

タイムアウト付きの Redux アクションをディスパッチするにはどうすればいいですか? 質問する

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

別のアクションを使用して返す方法がわからずsetTimeout、オンラインでその方法を見つけることができません。アドバイスがあれば歓迎します。

ベストアンサー1

陥らないように図書館があらゆることを規定すべきだと考える罠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()accept が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アクション クリエーターを にバインドすることはできません。Redux アクションを返しません。connect()showNotificationWithTimeout()

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

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

もしあなたがまだ私たちと一緒にいて、あなたのアプリに問題があると認識しているなら、リダックスサンクミドルウェア。

gist では、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)
  }
}

Note how the function is almost identical to the one we wrote in the previous section. However it doesn’t accept dispatch as the first argument. Instead it returns a function that accepts dispatch as the first argument.

How would we use it in our component? Definitely, we could write this:

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

We are calling the async action creator to get the inner function that wants just dispatch, and then we pass dispatch.

However this is even more awkward than the original version! Why did we even go that way?

Because of what I told you before. If Redux Thunk middleware is enabled, any time you attempt to dispatch a function instead of an action object, the middleware will call that function with dispatch method itself as the first argument.

So we can do this instead:

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

Finally, dispatching an asynchronous action (really, a series of actions) looks no different than dispatching a single action synchronously to the component. Which is good because components shouldn’t care whether something happens synchronously or asynchronously. We just abstracted that away.

Notice that since we “taught” Redux to recognize such “special” action creators (we call them thunk action creators), we can now use them in any place where we would use regular action creators. For example, we can use them with 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)

Reading State in Thunks

Usually your reducers contain the business logic for determining the next state. However, reducers only kick in after the actions are dispatched. What if you have a side effect (such as calling an API) in a thunk action creator, and you want to prevent it under some condition?

Without using the thunk middleware, you’d just do this check inside the component:

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

However, the point of extracting an action creator was to centralize this repetitive logic across many components. Fortunately, Redux Thunk offers you a way to read the current state of the Redux store. In addition to dispatch, it also passes getState as the second argument to the function you return from your thunk action creator. This lets the thunk read the current state of the store.

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)
  }
}

Don’t abuse this pattern. It is good for bailing out of API calls when there is cached data available, but it is not a very good foundation to build your business logic upon. If you use getState() only to conditionally dispatch different actions, consider putting the business logic into the reducers instead.

Next Steps

Now that you have a basic intuition about how thunks work, check out Redux async example which uses them.

You may find many examples in which thunks return Promises. This is not required but can be very convenient. Redux doesn’t care what you return from a thunk, but it gives you its return value from dispatch(). This is why you can return a Promise from a thunk and wait for it to complete by calling dispatch(someThunkReturningPromise()).then(...).

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

一部のアプリでは、非同期制御フローの要件が複雑すぎてサンクで表現できない状況に陥ることがあります。たとえば、失敗したリクエストの再試行、トークンを使用した再認証フロー、またはステップバイステップのオンボーディングは、この方法で記述すると冗長になり、エラーが発生しやすくなります。この場合、次のようなより高度な非同期制御フローソリューションを検討することをお勧めします。リダックスサーガまたはリダックスループそれらを評価し、自分のニーズに関連する例を比較して、最も気に入ったものを選択してください。

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

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

なぜこれをやっているのか分かっていない限り、心配しないでください。

おすすめ記事