1. ホーム
  2. javascript

[解決済み] Reduxのアクションをタイムアウトでディスパッチする方法とは?

2022-03-18 02:33:20

質問

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

を使ってもうまくいかなかった。 setTimeout を実行し、別のアクションを返すという方法が、オンラインでは見つかりません。どんなアドバイスでも歓迎します。

解決方法を教えてください。

に陥らないようにしましょう。 ライブラリは、すべてをどのように行うかを規定すべきであると考えるという罠 . JavaScript でタイムアウトを使用して何かをしたい場合、次のようにします。 setTimeout . 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 を第1引数として指定するのですか?それは、ストアにアクションをディスパッチする必要があるからです。通常、コンポーネントがアクセスできるのは 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.')    

これにより、ロジックの重複の問題が解決され、レースコンディションから解放されます。

Thunkミドルウェア

シンプルなアプリの場合、このアプローチで十分です。それで満足なら、ミドルウェアのことは気にしなくていい。

しかし、大規模なアプリでは、その周辺にある種の不都合を感じるかもしれません。

たとえば、このように dispatch を持ち歩きます。そのため、よりやっかいなのは コンテナとプレゼンテーショナルコンポーネントの分離 なぜなら、上記の方法でReduxのアクションを非同期でディスパッチするコンポーネントはすべて dispatch をプロップとして渡すことができます。アクションの作成者を connect() というのも showNotificationWithTimeout() は本当の意味でのアクションクリエイターではありません。Reduxのアクションを返すわけではありません。

さらに、以下のような同期アクションを生成する関数を覚えておくのは面倒です。 showNotification() のような非同期ヘルパーである。 showNotificationWithTimeout() . 使い分けが必要で、間違えないように注意する必要があります。

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

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

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 メソッド自身を第1引数として与えます。 .

だから、代わりにこうすればいいんです。

// 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を呼び出すなど)があり、何らかの条件でそれを防ぎたい場合はどうしたらよいでしょうか。

thunkミドルウェアを使わなければ、このチェックをコンポーネント内部で行うだけです。

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

しかし、アクションクリエイターを抽出するポイントは、多くのコンポーネントにまたがるこの繰り返しロジックを一元化することでした。幸いなことに、Redux Thunkは以下のような方法を提供しています。 読む は、Reduxストアの現在の状態です。さらに dispatch を渡すと、さらに getState を、サンクのアクション作成者から返す関数の第2引数として指定します。これによってthunkはストアの現在の状態を読み取ることができます。

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() のみを使用して異なるアクションを条件付きでディスパッチする場合は、ビジネスロジックを代わりにreducersに入れることを検討してください。

次のステップ

サンクがどのように機能するかについての基本的な直感を得たので、Redux をチェックしてみましょう。 非同期の例 を使用しています。

サンクがプロミスを返す例はよく見かけると思います。これは必須ではありませんが、非常に便利です。Reduxはサンクが何を返すかは気にしませんが、サンクの返り値を dispatch() . このため、サンクからプロミスを返し、そのサンクが完了するのを待つために dispatch(someThunkReturningPromise()).then(...) .

また、複雑なサンクアクションの作成者をいくつかの小さなサンクアクションの作成者に分割することもできます。その場合 dispatch メソッドはthunks自身を受け入れることができるので、再帰的にパターンを適用することができます。繰り返しになりますが、これはPromiseと最も相性が良く、その上で非同期制御フローを実装することができるからです。

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

最後に、本当に必要でないものは(サンクも含めて)使わないでください。要件によっては、ソリューションが次のように単純に見えるかもしれないことを忘れないでください。

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

なぜこんなことをするのか、理由がわからなければ汗をかかないことです。