How we use ES6 generators to simplify our React Redux application code

By Chirag Swadia

Chirag is a Software Engineer at AUTO1 Group.

< Back to list
Coding

Background

When I first started working on React/Redux, the very first choice of library I used for handling asynchronous actions was Redux Thunk as its maintained by the authors of Redux, and is very popular as well. Consider the following example, where an action type is being created for incrementing a counter and a corresponding action creator for it.

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

const increment = () => ({
  type: INCREMENT_COUNTER
})

Using redux thunk, an async action creator can be created as follows

const incrementAsync = () => {
  return dispatch => {
    setTimeout(() => {
      dispatch(increment());
    }, 1000);
  };
}

Pretty simple till now.

The Problem

Redux Thunk is easy to understand, but the problem arises when the application scales, and as the need for dispatching condition-based actions grows with the application, it leads to unmaintainable code. For example, consider the below code

const makeSandwichesForEverybody = () => (dispatch, getState) => {
  if (!getState().sandwiches.isShopOpen) {
    return Promise.resolve();
  }

  return dispatch(
    makeASandwichWithSecretSauce('My Grandma')
  ).then(() =>
    Promise.all([
      dispatch(makeASandwichWithSecretSauce('Me')),
      dispatch(makeASandwichWithSecretSauce('My wife'))
    ])
  ).then(() =>
    dispatch(makeASandwichWithSecretSauce('Our kids'))
  ).then(() =>
    dispatch(getState().myMoney > 42 ?
      withdrawMoney(42) :
      apologize('Me', 'The Sandwich Shop')
    )
  );
};

In the above code, there are too many callbacks and the code does not look clean. Also, it would become complex if more conditions are added to it later on. Then, how to simplify this?

ES6 Generators to the rescue

For our customer-facing websites like wirkaufendeinauto and AutoHero, we are neither using Redux Thunk nor Redux Saga. Rather, we are using a very simple and lightweight npm module Redux Actions Generators developed by our very own Alexander Afonin, who works as a Senior Software Engineer at Auto1. If this module is configured as a middleware in the React application, the above code can be simplified as shown below

const makeSandwichesForEverybody = () => function* ({ getState }) {
  if (!getState().sandwiches.isShopOpen) {
    return;
  }

  yield makeASandwichWithSecretSauce('My Grandma');
  yield [makeASandwichWithSecretSauce('Me'), makeASandwichWithSecretSauce('My wife')];
  yield makeASandwichWithSecretSauce('Our kids');

  if (getState().myMoney > 42) {
    yield withdrawMoney(42):
  } else {
    yield apologize('Me', 'The Sandwich Shop');
  }
};

Now you are ready to tackle the growing complexity with ease. Also, the code is clean and readable.

One question that might arise is that why not use async/await with thunk actions which could have solved the problem that is being discussed earlier. But, there are advantages of using generators instead of async/await thunks which are listed below -

Testability

Redux Thunk returns promises, which are more difficult to test. Testing thunks often require complex mocking of the fetch API, Axios requests, or other functions. With generators, you do not need to mock functions wrapped with effects. This makes tests clean, readable and easier to write.

Error Handling

The best part about the Redux Actions Generator module is that you can catch all errors at one place, without the need to write multiple try catch blocks in different actions ( you can if you want to, but not mandatory ) With redux-thunk, this is not possible and you have to write error handlers everywhere, which makes the code look less readable. To use this common error catching functionality, you can configure it while creating the store as shown below -

const catchError = error => console.error(error); 
const middlewares = applyMiddleware(createGeneratorMiddleware(null, catchError));

Once you do this, your actions can become clean and more readable. For example, the below code...

const someAction = () => function* ({ api }) {
  try {
    yield api.loadSomeItems();
  } catch (error) {
    // handle error logic
    console.log(error);
  }
}

can be replaced with...

const someAction = () => function* ({ api }) {
  yield api.loadSomeItems();
}

In this way, all the uncaught errors will be caught with the catchError function.

Enough of Making Sandwiches

Let's get to some real life example on how we use this pattern in our code. Consider the Auto Kaufen page on our wirkaufendeinauto website where we show a list of ads, filters, header, footer and some other content.

For this module, we have some actions like

const loadAds = (filters) =>
  function*() {
    // filter is an object with some information like {make: 'Audi', model: 'A3'}
    
    const { ads: result } = yield api.findAds(filters);

    // Once we get the ads list, we just save it in the redux store
  }

const loadSimilarAds = (filters) =>
  function*(){
      //  this is same as loadAds, but the API endpoint from which it fetches data is different
  }

Similarly, we have other actions like loadDataFromCms, trackGtmEvents etc. Now the action which will be called when the main component mounts, will be as shown below

export const loadPage = () => 
  function*({ getState }){
      yield loadDataFromCms('header');

      const filters = yield getFiltersFromStore(getState());
      yield [
        loadAds(filters),
        loadMakeModelSubtype(filters),
        loadDataFromCms('home')
      ];

      // If no ads found, we try to find similar ads which might interest the user
      if( getState().ads.length === 0 ) {
          yield loadSimilarAds(filters);
      }
  }

As you can see, in the above code we are making multiple API calls, some are in parallel, and some are fired conditionally based on the results of the previous API calls. This way our redux actions look clean and are easy to debug and maintain.

Summary

So this is how you can make use of the ES6 generators to simplify your React application code. Comments and suggestions are welcome.

Stories you might like:
By Artur Yolchyan

Usage cases for spring-batch.

By Piotr Czekaj

How to write a simple IntelliJ plugin

By Mariusz Nowak

Improved Common Table Expressions in recent release of Postgres