Skip to content

Instantly share code, notes, and snippets.

@waterlow
Last active August 26, 2017 14:22
Show Gist options
  • Select an option

  • Save waterlow/fae68e04fe3eef9fed347f1e860c48cc to your computer and use it in GitHub Desktop.

Select an option

Save waterlow/fae68e04fe3eef9fed347f1e860c48cc to your computer and use it in GitHub Desktop.

ライブラリがすべてのことをする方法を処方しなければならないと考えることの罠に陥ってはいけません。 JavaScriptでタイムアウトを使用したい場合は、setTimeoutを使用する必要があります。 Reduxの動作が異なる必要はありません。

Reduxは、非同期のものを扱ういくつかの代替方法を提供していますが、あまりにも多くのコードを繰り返していることがわかったときにだけ、それらを使用してください。この問題がなければ、言語が提供するものを使用して、最も簡単な解決策に進んでください。

Writing Async Code Inline

Redux特有のことは何もありません。

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

同様に、connect()されたコンポーネントの内部から:

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

唯一の違いは、connect()されたコンポーネントでは、通常、store自体にアクセスすることはできませんが、dispatch()または特定のアクションクリエータをpropsとして注入することです。しかし、本質的な違いはありません。

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

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

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

Extracting Async Action Creator

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

  • このロジックを通知を表示する場所に複製する必要があります。
  • 通知には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 interval ID and call
  // clearInterval(), but we’d still want to do it in a single place.
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

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

これで各componentはshowNotificationWithTimeoutを使用できます。このロジックを複製したり、競合条件にさまざまな通知を加えたりする必要はありません。:

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

なぜshowNotificationWithTimeout()は最初の引数としてdispatchを受け入れますか?ストアにアクションをディスパッチする必要があるためです。通常、コンポーネントはディスパッチにアクセスできますが、外部関数がディスパッチを制御できるようにするために、ディスパッチを渡すす必要があります。 あるモジュールからシングルトンなstoreをエクスポートした場合、それをインポートして、代わりに直接ディスパッチすることができます

// 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 Middleware

単純なアプリの場合、そのアプローチで十分です。あなたがそれに満足すればミドルウェアを心配しないでください。

しかし、より大きなアプリでは、その周りにある不便さがあるかもしれません。

例えば、私たちはdispatchを渡す必要があることは残念です。これにより、コンテナとプレゼンテーションコンポーネントを分離するのが難しくなります。これは、上記のようにReduxアクションを非同期にディスパッチするコンポーネントは、ディスパッチをpropsとして受け入れなければならないため、さらにそれを渡すことができるからです。 showNotificationWithTimeout()はaction creatorではないため、アクション作成者をconnect()でバインドすることはできません。 Reduxアクションは返されません。 さらに、どの関数がshowNotification()のような同期アクションクリエータであり、どれがshowNotificationWithTimeout()のような非同期ヘルパーであるかを覚えておくのは厄介なことです。あなたはそれらを別々に使用しなければならず、お互いに間違えないように注意してください。

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

あなたがまだ私たちと一緒にいて、あなたのアプリで問題として認識している場合は、Redux Thunkミドルウェアを使用することは大歓迎です。

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 action creatorとして宣言できます。

// 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を受け入れる関数を返します。

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

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

私たちは非同期アクションクリエータを呼び出してディスパッチするだけの内部関数を取得し、ディスパッチを渡します。

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

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

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

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

Reading State in Thunks

通常、reducerには次の状態を決定するためのビジネスロジックが含まれています。しかし、減速機はアクションが派遣された後に始まります。thunk action creatorで副作用(APIを呼び出すなど)があり、何らかの状態でそれを防止したい場合はどうなりますか?

redux-thunkを使用しないと、コンポーネント内でこのチェックを行うだけです:

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

しかし、action creatorを抽出するというポイントは、この繰り返しロジックを多くのcomponentにわたって集中させることでした。幸いにも、Redux ThunkはReduxストアの現在の状態を読み取る方法を提供します。dispatchに加えて、あなたのサthunk action creatorから返す関数への第2引数としてgetStateを渡します。これにより、thunkは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)
  }
}

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

Next Steps

サンクの仕組みに関する基本的な直感が得られたので、それらを使用するRedux非同期の例を確認してください。

サンクが約束を返す多くの例があります。これは必須ではありませんが、非常に便利です。 Reduxはサンクから戻ったものを気にしませんが、dispatch()からの戻り値を返します。これは、サンクからPromiseを返し、dispatch(someThunkReturningPromise())、then(...)を呼び出すことで完了するまで待つことができる理由です。

複雑なサンクのアクションクリエイターをいくつかの小さなサンクアクションクリエーターに分割することもできます。 thunksによって提供されるディスパッチメソッドは、サンク自体を受け入れることができるので、パターンを再帰的に適用することができます。ここでも、これはプロミスに最も適しています。なぜなら、その上に非同期制御フローを実装できるからです。

一部のアプリケーションでは、非同期制御フローの要件が複雑すぎて、サンクで表現できない状況に遭遇することがあります。たとえば、失敗したリクエストの再試行、トークンによる再認証フロー、またはステップバイステップのオンボードは、このように記述するとあまりにも冗長でエラーが発生しやすくなります。この場合、Redux SagaやRedux Loopなど、より高度な非同期制御フローソリューションを検討することができます。それらを評価し、ニーズに関連する例を比較し、最も好きなものを選んでください。

最後に、本物の必要がない場合は何も使用しないでください(サンクを含む)。要件に応じて、ソリューションは次のように簡単に見えるかもしれません。

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment