JavaScript - - By Prasanna Mahendiran

Async Operations in React Redux Applications

For a high-quality, in-depth introduction to React, you can’t go past Canadian full-stack developer Wes Bos. Try his course here, and use the code SITEPOINT to get 25% off and to help support SitePoint.

This post was originally posted at Codebrahma.

JavaScript is a single-threaded programming language. That is, when you have code something like this …

async react redux

… the second line doesn’t get executed till the first one gets completed. Mostly this won’t be a problem, since millions of calculations are performed by the client or server in a second. We notice the effects only when we’re performing a costly calculation (a task that takes noticeable time to complete — a network request which takes some time to return back).

Why did I show only an API call (network request) here? What about other async operations? An API call is a very simple and useful example for describing how to deal with an asynchronous operation. There are other operations, like setTimeout(), performance-heavy calculations, image loading, and any event-driven operations.

While structuring our application, we need to consider how asynchronous execution impacts structuring. For example, consider fetch() as a function that performs an API call (network request) from the browser. (Forget if it is an AJAX request. Just think of the behavior as either asynchronous or synchronous in nature.) The time elapsed while the request is processed on the server doesn’t happen on the main thread. So your JS code will keep getting executed, and once the request returns a response it will update the thread.

Consider this code:

userId = fetch(userEndPoint); // Fetch userId from the userEndpoint
userDetails = fetch(userEndpoint, userId) // Fetch for this particular userId.

In this case, since fetch() is asynchronous, we won’t be having userId when we try to fetch userDetails. So we need to structure it in a way that ensures the second line executes only when the first returns a response.

Most modern implementations of network requests are asynchronous. But this doesn’t always help, since we depend on the previous API response data for the subsequent API calls. Let’s look at how particularly we can structure this in ReactJS/Redux applications.

React is a front-end library used for making user interfaces. Redux is a state container that can manage the whole state of the application. With React in combination with Redux, we can make efficient applications that scale well. There are several ways to structure async operations in such a React application. For each method, let’s discuss the pros and cons in relation to these factors:

  • code clarity
  • scalability
  • ease of error handling.

For each method, we’ll perform these two API calls:

1. Fetching city from userDetails (First API response)

Let’s assume the endpoint is /details. It will have the city in the response. The response will be an object:

userDetails : {
  …
  city: 'city',
  …
};

2. Based on the user city we will fetch all restaurants in the city

Let’s say the endpoint is /restuarants/:city. The response will be an array:

['restaurant1', 'restaurant2', …]

Remember that we can do the second request only when we finish doing the first (since it’s dependent on the first request). Let’s look at various ways to do this:

  • directly using promise or async await with setState
  • using Redux Thunk
  • using Redux-Saga
  • using Redux Observables.

Particularly I have chosen the above methods because they’re the most popularly used for a large-scale project. There are still other methods that can be more specific to particular tasks and that don’t have all the features required for a complex app (redux-async, redux-promise, redux-async-queue to name a few).

Promises

A promise is an object that may produce a single value some time in the future: either a resolved value, or a reason that it’s not resolved (e.g., a network error occurred). — Eric Elliot

In our case, we’ll use the axios library to fetch data, which returns a promise when we make a network request. That promise may resolve and return the response or throw an error. So, once the React Component mounts, we can straight away fetch like this:

componentDidMount() {
  axios.get('/details') // Get user details
    .then(response => {
    const userCity = response.city;
    axios.get(`/restaurants/${userCity}`)
      .then(restaurantResponse => {
       this.setState({
         listOfRestaurants: restaurantResponse, // Sets the state
       })
    })
  })
}

This way, when the state changes (due to fetching), Component will automatically re-render and load the list of restaurants.

Async/await is a new implementation with which we can make async operations. For example, the same thing can be achieved by this:

async componentDidMount() {
  const restaurantResponse = await axios.get('/details') // Get user details
    .then(response => {
    const userCity = response.city;
    axios.get(`/restaurants/${userCity}`)
      .then(restaurantResponse => restaurantResponse
    });

    this.setState({
      restaurantResponse,
    });
}

Both of these are the simplest of all methods. Since the entire logic is inside the component, we can easily fetch all the data once the component loads.

Drawbacks in the method

The problem will be when doing complex interactions based on the data. For example, consider the following cases:

async state questions

  • We don’t want the thread in which JS is being executed to be blocked for network request.
  • All the above cases will make the code very complex and difficult to maintain and test.
  • Also, scalability will be a big issue, since if we plan to change the flow of the app, we need to remove all the fetches from the component.
  • Imagine doing the same if the component is at the top of the parent child tree. Then we need to change all the data dependent presentational components.
  • Also to note, the entire business logic is inside the component.

How we can improve from here?

1. State Management
In these cases, using a global store will actually solve half of our problems. We’ll be using Redux as our global store.

2. Moving business logic to correct place
If we think of moving our business logic outside of the component, then where exactly can we do that? In actions? In reducers? Via middleware? The architecture of Redux is such that it’s synchronous in nature. The moment you dispatch an action (JS objects) and it reaches the store, the reducer acts upon it.

3. Ensuring there’s a separate thread where async code is executed and any change to global state can be retrieved through subscription

redux architecture

From this, we can get an idea that if we’re moving all the fetching logic before reducer — that is either action or middleware — then it’s possible to dispatch the correct action at the correct time.
For example, once the fetch starts, we can dispatch({ type: 'FETCH_STARTED' }), and when it completes, we can dispatch({ type: 'FETCH_SUCCESS' }).

Want to develop a React JS application?

Recommended Courses

Wes Bos
A step-by-step training course to get you building real world React.js + Firebase apps and website components in a couple of afternoons. Use coupon code 'SITEPOINT' at checkout to get 25% off.

Using Redux Thunk

Redux Thunk is middleware for Redux. It basically allows us to return function instead of objects as an action. This helps by providing dispatch and getState as arguments for the function. We use the dispatch effectively by dispatching the necessary actions at the right time. The benefits are:

  • allowing multiple dispatches inside the function
  • relating of business logic to the fetch will be outside of React components and moved to actions.

In our case, we can rewrite the action like this:

export const getRestaurants = () => {
  return (dispatch) => {
  dispatch(fetchStarted()); // fetchStarted() returns an action

  fetch('/details')
    .then((response) => {
      dispatch(fetchUserDetailsSuccess()); // fetchUserDetailsSuccess returns an action
      return response;
     })
    .then(details => details.city)
    .then(city => fetch('/restaurants/city'))
    .then((response) => {
      dispatch(fetchRestaurantsSuccess(response)) // fetchRestaurantsSuccess(response) returns an      action with the data
    })
    .catch(() => dispatch(fetchError())); // fetchError() returns an action with error object
  };
}

As you can see, we now have a good control of when to dispatch what type of action. Each function call like fetchStarted(), fetchUserDetailsSuccess(), fetchRestaurantsSuccess() and fetchError() dispatches a plain JavaScript object of a type and additional details if required. So now it’s the job of the reducers to handle each action and update the view. I haven’t discussed the reducer, since it’s straightforward from here and the implementation might be varying.

For this to work, we need to connect the React component with Redux and bind the action with the component using the Redux library. Once this is done, we can simply call this.props.getRestaurants(), which in turn will handle all the above tasks and update the view based on the reducer.

In terms of its scalability, Redux Thunk can be used in apps which don’t involve complex controls over async actions. Also, it works seamlessly with other libraries, as discussed in the topics of the next section.

But still, it’s a little difficult to do certain tasks using Redux Thunk. For example, we need to pause the fetch in between, or when there are multiple such calls, and allow only the latest, or if some other API fetches this data and we need to cancel.

We can still implement those, but it will be little complicated to do exactly. Code clarity for complex tasks will be little poor when compared with other libraries, and maintaining it will be difficult.

Using Redux-Saga

Using the Redux-Saga middleware, we can get additional benefits that solve most of the above-mentioned functionalities. Redux-Saga was developed based on ES6 generators.

Redux-Saga provides an API that helps to achieve the following:

  • blocking events that block the thread in the same line till something is achieved
  • non-blocking events that make the code async
  • handling race between multiple async requests
  • pausing/throttling/debouncing any action.

How do sagas work?

Sagas use a combination of ES6 generators and async await APIs to simplify async operations. It basically does its work on a separate thread where we can do multiple API calls. We can use their API to make each call synchronous or asynchronous depending on the use case. The API provides functionalities by which we can make the thread to wait in the same line till the request returns a response. Apart from this, there are lot of other APIs provided by this library, which makes API requests very easy to handle.

Consider our previous example: if we initialize a saga and configure it with Redux as mentioned in their documentation, we can do something like this:

import { takeEvery, call } from 'redux-saga/effects';
import request from 'axios';

function* fetchRestaurantSaga() {

  // Dispatches this action once started
  yield put({ type: 'FETCH_RESTAURANTS_INITIATED '});

  try {
    // config for fetching details API
    const detailsApiConfig = {
      method: 'get',
      url: '/details'
    };
    // Blocks the code at this line till it is executed
    const userDetails = yield call(request, config);

    // config for fetching details API
    const restaurantsApiConfig = (city) {
      method: 'get',
      url: `/restaurants/${city}`,
    };

    // Fetches all restuarants
    const restaurants = yield call(request, restaurantsApiConfig(userDetails.city));

    // On success dispatch the restaurants
    yield put({
      type: 'FETCH_RESTAURANTS_SUCCESS',
      payload: {
        restaurants
      },
    });

  } catch (e) {
    // On error dispatch the error message
    yield put({
      type: 'FETCH_RESTAURANTS_ERROR',
      payload: {
        errorMessage: e,
      }
    });
  }
}

export default function* fetchRestaurantSagaMonitor() {
  yield takeEvery('FETCH_RESTAURANTS', fetchInitial); // Takes every such request
}

So if we dispatch a simple action with type FETCH_RESTAURANTS, the Saga middleware will listen and respond. Actually, none of the Actions get consumed by the middleware. It just listens and does some additional tasks and dispatches a new action if required. By using this architecture, we can dispatch multiple requests each describing

  • when the first request started
  • when the first request finished
  • when the second request started

… and so on.

Also, you can see the beauty of fetchRestaurantsSaga(). We have currently used a call API for implementing blocking calls. Sagas provide other APIs, like fork(), which implements non-blocking calls. We can combine both blocking and nonblocking calls to maintain a structure that fits our application.

In terms of scalability, using sagas is beneficial:

  • We can structure and group sagas based on any particular tasks. We can trigger one saga from another by simply dispatching an action.
  • Since it’s middleware, actions that we write will be plain JS objects, unlike thunks.
  • Since we move the business logic inside sagas (which is a middleware), if we know what will be the functionality of a saga, then understanding the React part of it will be much easier.
  • Errors can easily be monitored and dispatched to the store through a try/catch pattern.

Using Redux-Observables

As mentioned in their documentation under “An epic is the core primitive of redux-observable”:

  1. An Epic is a function that takes a stream of actions and returns a stream of actions. That is, an Epic runs alongside a normal Redux dispatch channel, after the reducers have already received them.

  2. Actions always run through your reducers before epics even receive them. An Epic just receives and outputs another stream of actions. This is similar to Redux-Saga, in that none of the Actions get consumed by the middleware. It just listens and does some additional tasks.

For our task, we can simply write this:

const fetchUserDetails = action$ => (
  action$.ofType('FETCH_RESTAURANTS')
    .switchMap(() =>
      ajax.getJSON('/details')
        .map(response => response.userDetails.city)
        .switchMap(() =>
          ajax.getJSON(`/restaurants/city/`)
            .map(response => ({ type: 'FETCH_RESTAURANTS_SUCCESS', payload: response.restaurants })) // Dispatching after success
)
         .catch(error => Observable.of({ type: 'FETCH_USER_DETAILS_FAILURE', error }))
      )
    )
)

At first, this might look little confusing. But the more you understand RxJS, the easier it is to create an Epic.

As in the case of sagas, we can dispatch multiple actions each one describing at what part of the API request chain the thread currently is in.

In terms of scalability, we can split Epics or compose Epics based on particular tasks. So this library can help in building scalable applications. Code clarity is good if we understand the Observable pattern of writing code.

My Preferences

How do you determine which library to use?
It depends on how complex our API requests are.

How do you choose between Redux-Saga and Redux-Observable?
It comes down to the learning generators or RxJS. Both are different concepts but equally good enough. I would suggest trying both to see which one suits you best.

Where do you keep your business logic dealing with APIs?
Preferably before the reducer, but not in the component. The best way would be in middleware (using sagas or observables).

You can read more React Development posts at Codebrahma.