DEV Community

Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Managing asynchronous actions in Redux

Async actions in Redux with Thunk or custom middleware

As you know, Redux provides you with an elegant approach to manage the state of a JavaScript application. Its infrastructure is based on functional foundations and lets you easily build testable code. However, the flow of Redux tasks to manage the state of an application is completely synchronous: dispatching an action immediately generates the chain of calls to middlewares and reducers to carry out the state transition.

How can we enable the state transition of an application via asynchronous actions? How to enable, for example, state transitions involving a request to a Web server or the use of a timer? How do we integrate our application state with the data generated by an asynchronous action, while complying to the Redux architectural pattern?

Splitting the asynchronous action

The common approach to integrate asynchronous tasks into the Redux architecture is to break an asynchronous action into at least three synchronous actions.

An action informing that the asynchronous task:

  • started
  • was successfully completed
  • failed

Each of these actions changes the application state and keeps it in line with what is happening during the asynchronous task execution.

Implementing this approach requires that you dispatch the action that starts the asynchronous task. When the asynchronous task ends, a callback should manage the outcome of the asynchronous task and appropriately update the state with a positive or negative response.

That said, you may be tempted to support asynchronous actions by modifying their reducers, i.e. making sure that the reducer intercepting that action starts the asynchronous task and manages its outcome. However, this implementation violates the constraint that states a reducer to be a pure function. In fact, the result of an asynchronous task by its nature is based on a side effect.

So, let’s take a look at a couple of valid solutions to this problem.

Asynchronous actions and Thunk

A first approach is based on the Thunk middleware. The role of this middleware is very simple: verify if an action is a function and in which case execute it. This simple behaviour allows us to create actions no longer as simple objects, but as functions, which therefore have business logic.

So, in order to solve our problem with asynchronous tasks, we can define an action as a function that starts an asynchronous task and delegates its execution to the thunk middleware. Unlike the reducer, middleware is not required to be a pure function, so the thunk middleware can perform functions that trigger side effects without any problem.

Let’s put these concepts into practice by implementing a simple application that shows a Ron Swanson random quote from a specialized API. The markup of the Web page appears as follows:

<div>
  Ron Swanson says:
  <blockquote id="quote"></blockquote>
</div>
Enter fullscreen mode Exit fullscreen mode

For the JavaScript side, you need to get the redux and redux-thunk dependencies and import a few items in the module as shown below:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
Enter fullscreen mode Exit fullscreen mode

As said before, you must first define three synchronous actions that represent changes in the state during the execution of the asynchronous task. Let’s define the following constants:

const QUOTE_REQUESTED = "QUOTE_REQUESTED";
const QUOTE_RECEIVED = "QUOTE_RECEIVED";
const QUOTE_FAILED = "QUOTE_FAILED";
Enter fullscreen mode Exit fullscreen mode

As you can see, they represent the three phases we described above.

Let’s now define an action creator for Thunk:

function getQuoteAction() {
  return function(dispatch) {
    dispatch({
      type: QUOTE_REQUESTED,
    });

  fetch("https://ron-swanson-quotes.herokuapp.com/v2/quotes")
    .then(response => response.json())
    .then(data => dispatch({
        type: QUOTE_RECEIVED,
        payload: data
      }))
    .catch(error => dispatch({
        type: QUOTE_FAILED,
        payload: error
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The first thing you can notice is that the action creator getQuoteAction() returns a function, as expected. The returned function starts dispatching the synchronous action QUOTE_REQUESTED and executes fetch() to actually start the asynchronous task, the HTTP request. Then, it dispatches one of the other two synchronous actions accordingly to the outcome of the asynchronous HTTP request.

Once we defined the transformation of an asynchronous action into three synchronous actions, we need to manage their impact on state transitions. So, let’s define the initial state of our application and the reducer that will manage quote retrieving:

const initialState = { data: [], status:"" };

function quotes(state = initialState, action) {
  switch (action.type) {
    case QUOTE_REQUESTED:
      state = Object.assign({}, state, {status: "waiting"});
      break;
    case QUOTE_RECEIVED:
      state = Object.assign({}, state, {data: [action.payload], status: "received"});
      break;
    case QUOTE_FAILED:
      state = Object.assign({}, state, {status: "failed", error: action.payload});
    break;
  }

  return state;
}
Enter fullscreen mode Exit fullscreen mode

The structure of the application state consists of a data array, containing the list of quotes to show (in our case we will have one quote), and a status string, representing the current status of the asynchronous action. The status property is not strictly required for the correct behaviour of the application, but it may be useful in order to give feedback to the user. The quotes() function implements a standard reducer by handling the three synchronous actions and generating the new application state accordingly.

The next step is to create the Redux store by specifying the use of the Thunk middleware, as shown by the following statement:

let store = createStore(quotes, initialState, applyMiddleware(thunk));
Enter fullscreen mode Exit fullscreen mode

Finally, you have to manage the UI connecting it to the Redux store, as the following code shows:

const quoteElement = document.getElementById("quote");

store.dispatch(getQuoteAction());
store.subscribe(() => {
  const state = store.getState();

  if (state.status == "waiting") {
    quoteElement.innerHTML = "Loading…";
  }
  if (state.status == "received") {
    quoteElement.innerHTML = state.data[0];
  }
});
Enter fullscreen mode Exit fullscreen mode

As you can see, you dispatch the starting action by calling the getQuoteAction() creator and then subscribe to state changes. When a state change occurs, you check the status property value and inject the text inside the blockquote HTML element accordingly.

The final result in your browser will look like the following:

Try this code on CodePen.

Creating your own custom middleware

Redux Thunk elegantly solves the problem of managing asynchronous actions in Redux. However, it forces you to make the action creator’s code more complicated by sending the HTTP request and handling the response.

If your application heavily interacts with the server, as often it happens, you will have a lot of duplicate or quite similar code within the action creators. This distorts the original purpose of the action creators: creating an action based on parameters. Perhaps, in these cases, it is more appropriate to create an ad hoc middleware. The goal is to isolate the code that makes HTTP requests to the server in a special middleware and to restore the action creator to its original job.

So, let’s define a constant that identifies a meta-action for the HTTP request. We call it a meta-action because it is not the action that will directly modify the application state. Indeed, it is an action that will trigger an HTTP request and which will cause changes to the application state as a side effect by generating other actions.

The following is our constant definition:

const HTTP_ACTION = "HTTP_ACTION";
Enter fullscreen mode Exit fullscreen mode

Along with this constant, you need to define the constants that identify the actual action and the related synchronous actions to implement the HTTP requests, as we have seen before:

const QUOTE = "QUOTE"
const QUOTE_REQUESTED = "QUOTE_REQUESTED";
const QUOTE_RECEIVED = "QUOTE_RECEIVED";
const QUOTE_FAILED = "QUOTE_FAILED";
Enter fullscreen mode Exit fullscreen mode

Now, you need the meta-action creator, that is an action creator that takes a plain action object as input and wraps it in order to create an asynchronous action to be handled via HTTP. The following is the meta-action creator that we are going to use:

function httpAction(action) {
  const httpActionTemplate = {
    type: "",
    endpoint: null,
    verb: "GET",
    payload: null,
    headers: []
  };

  return {
    HTTP_ACTION: Object.assign({}, httpActionTemplate, action)
  };
}
Enter fullscreen mode Exit fullscreen mode

You may notice that it returns an object with the HTTP_ACTION constant as its only property. The value of this property comes out from the action passed as a parameter combined with the action template. You notice that this template contains the general options for an HTTP request.

You will use this meta-action creator whenever you want to create an asynchronous action that will involve an HTTP request. For example, in order to apply this approach to retrieve the Ron Swanson random quotes described before, you can use the following action creator:

function getQuoteAction() {
  return httpAction({
    type: QUOTE,
    endpoint: "https://ron-swanson-quotes.herokuapp.com/v2/quotes"
  });
}

Enter fullscreen mode Exit fullscreen mode

As you can see, any asynchronous action that involves an HTTP request can be defined by invoking the httpAction() meta-action creator with the minimal data to build up the request. You no longer need to add here the logic of synchronous actions generation, because it was moved into the custom middleware, as shown by the following code:

const httpMiddleware = store => next => action => {
  if (action[HTTP_ACTION]) {
    const actionInfo = action[HTTP_ACTION];
    const fetchOptions = {
      method: actionInfo.verb,
      headers: actionInfo.headers,
      body: actionInfo.payload || null
    };

    next({
      type: actionInfo.type + "_REQUESTED"
    });

    fetch(actionInfo.endpoint, fetchOptions)
      .then(response => response.json())
      .then(data => next({
        type: actionInfo.type + "_RECEIVED",
        payload: data
      }))
      .catch(error => next({
        type: actionInfo.type + "_FAILED",
        payload: error
     }));
  } else {
    return next(action);
  }
}
Enter fullscreen mode Exit fullscreen mode

The middleware looks for the HTTP_ACTION identifier and replaces the current action with a brand new action with the _REQUESTED suffix. This new action is inserted in the middleware pipeline via next(). Then, it sends the HTTP request to the server and waits for a response or a failure. When one of these events occurs, the middleware generates the RECEIVED or FAILED actions as in the thunk-based approach.

At this point, the only thing you need to change to achieve the same result as in the thunk-based approach is the store creation:

let store = createStore(quotes, initialState, applyMiddleware(httpMiddleware));
Enter fullscreen mode Exit fullscreen mode

You are saying Redux to create the store by applying your custom httpMiddleware instead of the Thunk middleware. The implementation of the reducer and the UI management remain as before.

You can try the implementation of this approach on CodePen.

Conclusion

In summary, we discovered that any asynchronous action can be split in at least three synchronous actions. We exploited this principle to implement two approaches for managing asynchronous actions while using Redux.

You may consider the first approach, based on the standard Thunk middleware, easier, but it forces you to alter the original nature of an action creator.

The second approach, based on a custom middleware, may seem more complex at a first glance, but it is much more scalable and maintainable.


Plug: LogRocket, a DVR for web apps

https://logrocket.com/signup/

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.


The post Async actions in Redux with Thunk or custom middleware appeared first on LogRocket Blog.

Top comments (1)

Collapse
 
whataluckyguy profile image
Lalit Kumar

I also got that on another site

kodlogs.com/34843/error-actions-mu...

Solution :
I had also faced the similar issue in the recent past. I did lot of research on it and found the solution on it. This is the very common problem with the people getting started.

You must dispatch after async request ends.

This program would work:

export function bindAllComments(postAllId) {
return function (dispatch){
return API.fetchComments(postAllId).then(comments => {
// dispatch
dispatch( {
type: BIND_COMMENTS,
comments,
postAllId
})
})
}
}