Posted at

Reduxで非同期処理を行う方法を解説した記事の翻訳

More than 3 years have passed since last update.


動機

学生時代は英語が一番苦手でしたが、いまいちredux-thunkが理解できなかったので頑張って翻訳しました。おかしな訳があればご指摘ください。また、分からないところは原文の言葉を使うか省略しておりますのでご了承ください。

元記事:

http://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559


翻訳


Writing Async Code Inline


非同期コードについて書く


This is by far the simplest way. And there’s nothing specific to Redux here.


これは最も簡単な方法です。そして、これはRedux固有のものではありません。

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)


Similarly, from inside a connected component:


同様に「a connected component」の内側から

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {
this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)


The only difference is that in a connected component you usually don’t have access to the store itself, but get either dispatch() or specific action creators injected as props. However this doesn’t make any difference for us.


唯一の違いは、「a connected component」の中ではあなたは通常store自体にアクセスできませんが、dispatch()かpropsとして注入された特定のアクションクリエーターのいずれかを取得することです。しかし、私たちにとって違いはありません。


If you don’t like making typos when dispatching the same actions from different components, you might want to extract action creators instead of dispatching action objects inline:


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

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


Or, if you have previously bound them with connect():


また、あなたが以前connect()を使ってそれらを結合した場合:

this.props.showNotification('You just logged in.')

setTimeout(() => {
this.props.hideNotification()
}, 5000)


So far we have not used any middleware or other advanced concept.


ここまではミドルウェアや高度な概念を使用してはいません。


Extracting Async Action Creator


非同期アクションクリエータを抽出する


The approach above works fine in simple cases but you might find that it has a few problems:


単純な例では上記のようなアプローチは正常に動作するかもしれませんが、あなたはいくつかの問題を見つけるかもしれません。


It forces you to duplicate this logic anywhere you want to show a notification.



  • あなたが通知を表示したい場所にこのロジックの複製を強制します。


The notifications have no IDs so you’ll have a race condition if you show two notifications fast enough. When the first timeout finishes, it will dispatch HIDE_NOTIIFICATION, erroneously hiding the second notification sooner than after the timeout.



  • もし、あなたが十分な早さで2つの通知を表示させたら、その通知はIDを持っていないので、競合状態になるでしょう。最初のタイムアウトが完了したとき、HIDE_NOTIFICATIONがディスパッチされます。そして、それは誤ってタイムアウト後より早い2つ目の通知を隠します。


To solve these problems, you would need to extract a function that centralizes the timeout logic and dispatches those two actions. It might look like this:


これらの問題を解決するために、あなたはタイムアウトのロジックと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)
}


Now components can use showNotificationWithTimeout without duplicating this logic or having race conditions with different notifications:


このコンポーネントはロジックの重複や異なる通知の競合状態なしにshowNotificationWithTimeoutを利用することができます。

// component.js

showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

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


Why does showNotificationWithTimeout() accept dispatch as the first argument? Because it needs to dispatch actions to the store. Normally a component has access to dispatch but since we want an external function to take control over dispatching, we need to give it control over dispatching.


なぜ最初の引数としてshowNotificationWithTimeout()でdispatchを受け入れているのでしょうか。それはstoreにアクションをディスパッチする必要があるためです。通常、コンポーネントはディスパッチにアクセスする権限を持っていますが、私たちは外部の機能にディスパッチのコントロールを持ってもらいたいので、ディスパッチのコントロールを与える必要があります。


If you had a singleton store exported from some module, you could just import it and dispatch directly on it instead:


もし、あなたがモジュールからエクスポートされたシングルトンの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.')


This looks simpler but we don’t recommend this approach. The main reason we dislike it is because it forces store to be a singleton. This makes it very hard to implement server rendering. On the server, you will want each request to have its own store, so that different users get different preloaded data.


これは一見単純ですが、私たちはこのアプローチを推奨しません。主な理由はstoreがシングルトンであることを強制するのを嫌うからです。これはサーバーのレンダリングを実装することが非常に難しくなります。サーバー上であなたは各々のリクエストがそれ自身のstoreをもつことを望み、その結果、ユーザは異なった事前ロードデータを取得します。


A singleton store also makes testing harder. You can no longer mock a store when testing action creators because they reference a specific real store exported from a specific module. You can’t even reset its state from outside.


シングルトンのstoreはテストもまた難しくなります。あなたはアクションクリエーターをテストするときもはやstoreをモック化できません。なぜなら、それらは特定のモジュールからエクスポートされた特定のストアを参照しているからです。あなたは外側から状態をリセットすることさえできません。


So while you technically can export a singleton store from a module, we discourage it. Don’t do this unless you are sure that your app will never add server rendering.


そして、モジュールからシングルトンのstoreを技術的にエクスポートできる間、私たちはそれを阻止します。あなたのアプリが決してレンダリングを追加しないと確信してない限りこれをやってはいけません。


Getting back to the previous version:


前のバージョンに戻りましょう。

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


This solves the problems with duplication of logic and saves us from race conditions.


これはロジックの複製の問題を解決し、競合状態から私たちを救います。


Thunk Middleware


Thunkミドルウェア


For simple apps, the approach should suffice. Don’t worry about middleware if you’re happy with it.


シンプルなアプリケーションの場合、これまでのアプローチで十分です。あなたが満足しているなら、ミドルウェアの心配はしないでください。


In larger apps, however, you might find certain inconveniences around it.


しかし大きなアプリケーションの場合、あなたはそのアプローチに不便な部分を見つけるかもしれません。


For example, it seems unfortunate that we have to pass dispatch around. This makes it trickier to separate container and presentational components because any component that dispatches Redux actions asynchronously in the manner above has to accept dispatch as a prop so it can pass it further. You can’t just bind action creators with connect() anymore because showNotificationWithTimeout() is not really an action creator. It does not return a Redux action.


例えば、ディスパッチを持ち回すことは残念に感じます。Reduxアクションを非同期にディスパッチする任意のコンポーネントは、ディスパッチをさらに渡せるようにpropとしてディスパッチを許可するので、コンテナとプレゼンテーションコンポーネントに分離することをより難しくする。showNotificationWithTimeout()は実際のアクションクリエータではないので、あなたはもはやアクションクリエータをバインドすることはできません。それはReduxアクションを返しません。


In addition, it can be awkward to remember which functions are synchronous action creators like showNotification() and which are asynchronous helpers like showNotificationWithTimeout(). You have to use them differently and be careful not to mistake them with each other.


さらに、showNotification()のような同期アクションクリエーターの機能とshowNotificationWithTimeout()のよう非同期なヘルパーを覚えるのは厄介なことかもしれません。あなたは、それらを互いに間違えないように注意し、別々に使用しなければなりません。


This was the motivation for finding a way to “legitimize” this pattern of providing dispatch to a helper function, and help Redux “see” such asynchronous action creators as a special case of normal action creators rather than totally different functions.

If you’re still with us and you also recognize as a problem in your app, you are welcome to use the Redux Thunk middleware.


もし、あなたがまだ私たちとあなたのアプリの問題として認識するなら、あなたはRedux-Thunkミドルウェアを利用できます。


In a gist, Redux Thunk teaches Redux to recognize special kinds of actions that are in fact functions:


要するに、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)
})


When this middleware is enabled, if you dispatch a function, Redux Thunk middleware will give it dispatch as an argument. It will also “swallow” such actions so don’t worry about your reducers receiving weird function arguments. Your reducers will only receive plain object actions—either emitted directly, or emitted by the functions as we just described.


このミドルウェアが有効なとき、もしあなたがメソッドをディスパッチするなら、Redux-Thunkミドルウェアは引数としてdispatchを与えます。そのようなアクションを「飲み込む」ので、奇妙なメソッドの引数を受け取るあなたのreducerについて心配ありません。あなたのreducerは直接作成されたか、後述する関数によって作成されたかしたプレーンオブジェクトだけを受け取るでしょう。


This does not look very useful, does it? Not in this particular situation. However it lets us declare showNotificationWithTimeout() as a regular Redux action creator:


これはあまり有益に見えませんね。特定の状況ではありません。しかし、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.


関数は前節に書いたものとほぼ同じであるかに注意してください。しかし、最初の引数としてdispatchが許可されていません。代わりに最初の引数としてdispatchを許可する関数を返却します。


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.


私たちはdispatchを望む内部関数を取得するための非同期アクションクリエータを呼びます。そしてそのとき私たちは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.


私が前にあなたに言ったことのためです。もしRedux-Thunkミドルウェアが有効なら、いつでもあなたはアクションオブジェクトの代わりに関数をディスパッチすることを試みます。そのミドルウェアは最初の引数としてディスパッチ関数自身で関数を呼び出します。


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():


私たちはそのような「特別」のアクションクリエータを理解するようにReduxに「教えた」(それらをthunkアクションクリエータと呼ぶ)ということに注意してください。私たちは通常のアクションクリエータを利用する場所でそれらを利用することがでできる。例えば、私たちは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


thunkの状態を読み込む


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?


通常あなたのreducerは次の状態を決定するためにビジネスロジックを含んでいます。しかし、reducerはアクションがディスパッチされたあとにだけキックされます。仮にあなたがthunkアクションクリエータで副作用(例えばAPIを呼ぶとき)をもっていたら、いくつかの条件下でそれを防ぎたいです。

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

thunkミドルウェアを使用せずに、あなたはまさにコンポーネントの中をチェックするでしょう。

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


しかし、そのアクションクリエータを抽出するポイントは多くのコンポーネントで繰り返しロジックを集中管理することでした。幸いなことに、Redux-thunkはあなたにReduxのstoreの現在の状態を読み出すための方法を提供しています。ディスパッチに加えて、あなたのthunkアクションクリエータから返す関数に第二引数として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)
}
}


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.


このパターンを誤用しないでください。有効なキャッシュデータがある時にAPIコールの救済にはよいですが、ビジネスロジックを構築するためのよい基盤ではありません。もし、あなたがgetState()を条件付きで異なるアクションをディスパッチするために使うなら、代わりにreducerの中にビジネスロジックを置くことを検討してください。


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(...).

You may also split complex thunk action creators into several smaller thunk action creators. The dispatch method provided by thunks can accept thunks itself, so you can apply the pattern recursively. Again, this works best with Promises because you can implement asynchronous control flow on top of that.

For some apps, you may find yourself in a situation where your asynchronous control flow requirements are too complex to be expressed with thunks. For example, retrying failed requests, reauthorization flow with tokens, or a step-by-step onboarding can be too verbose and error-prone when written this way. In this case, you might want to look at more advanced asynchronous control flow solutions such as Redux Saga or Redux Loop. Evaluate them, compare the examples relevant to your needs, and pick the one you like the most.

Finally, don’t use anything (including thunks) if you don’t have the genuine need for them. Remember that, depending on the requirements, your solution might look as simple as


store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)


Don’t sweat it unless you know why you’re doing this.