Asynchronous Actions
In the previous tutorials, we built a todo application. It was entirely synchronous, where the state updated immediately every time an action was dispatched. Next, we’re going to build a different, asynchronous application. It will use the Reddit API to display headlines for a selected subreddit.
Actions
When you call an asynchronous API, there are two crucial moments: the moment you start the call, and the moment you receive a response.
Both of these moments typically require changes to the application’s state; for this, you need to dispatch regular actions that will be processed synchronously by reducers. Generally, for any API request, you will need to dispatch at least three different actions:
- An action notifying reducers that the request has started.
Reducers might handle this action by toggling an
isFetching
flag in the state, signaling the UI that it’s time to display a loading indicator. - An action notifying reducers that the request has successfully completed.
Reducers might handle this action by merging the new data into the state they control and resetting
isFetching
. The UI would then hide the loading indicator and display the fetched data. - An action notifying reducers that the request has failed.
Reducers might handle this action by resetting
isFetching
. Additionally, some reducers might want to store the error message so the UI can display it.
You might want to add a dedicated status
field to your actions:
{type: 'FETCH_POSTS'}
{type: 'FETCH_POSTS', status: 'error', error: 'Oops'}
{typs: 'FETCH_POSTS', status: 'success', response: { ... }}
Or define separate types for them:
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
The choice between a single action type with flags or multiple action types is up to you. Multiple types can lead to fewer errors, but this isn’t an issue if you use a helper library like redux-actions to generate action creators and reducers.
Asynchronous Data Flow
Without middleware, a Redux store only provides synchronous data flow. This is the default result you get from createStore()
.
You can enhance createStore()
using applyMiddleware()
. This isn’t strictly necessary, but it allows you to describe asynchronous actions in a convenient way.
Asynchronous middleware like redux-thunk
or redux-promise
wraps the store’s dispatch()
method, allowing you to dispatch things other than actions, such as functions or Promises. The middleware you use can interpret whatever you dispatch in its own way and continue passing actions to the next middleware. For example, Promise-aware middleware can intercept Promises and then asynchronously dispatch a pair of begin/end actions for each Promise.
When the last middleware in the data flow dispatches an action, it must be a plain object. This is where the synchronous Redux data flow begins.
Middleware
Middleware provides a third-party extension point between dispatching an action and it reaching the reducer. People use middleware for logging, crash reporting, calling an asynchronous API, routing… Here are some examples to demonstrate the power of middleware.
Understanding Middleware
Middleware can do many things, and it’s important to understand where it comes from. We’ll use logging and crash reporting as two examples to illustrate the thought process behind using middleware.
Problem: Logging
Attempt #1: Manual Logging
The most primitive solution is to log the action and the next state every time store.dispatch(action)
is called. The code might look like this:
let action = addTodo("Use Redux");
console.log("dispatching", action);
store.dispatch(action);
console.log("next state", store.getState());
This is a really clunky approach.
Attempt #2: Wrapping Dispatch
Extract logging into a function:
function dispatchAndLog(store, action) {
console.log("dispatching", action);
store.dispatch(action);
console.log("next state", store.getState());
}
Then, replace store.dispatch()
by using this function every time you call it.
dispatchAndLog(store, addTOdo("USe Redux"));
This solves the problem, but it’s not elegant.
Attempt #3: Monkeypatching Dispatch
What if we just replace the dispatch
function inside the store? A Redux store is just a plain object with a few methods, so we can overwrite dispatch
.
let next = store.dispatch
store.dispath = fucntion dispatchAndLog (action){
console.log('dispatching',action)
let result = next(action)
console.log('next state',soter.getState())
return reslut
}
This pretty much meets our requirements: no matter where you dispatch an action, it’s guaranteed to be logged. While we’ve monkeypatched the store’s internal method, let’s stick with this for now.
This article was published on May 22, 2016 and last updated on May 22, 2016, 3424 days ago. The content may be outdated.