How to Improve React+Redux Code With Redux Thunk Package

React + Redux are widely-used and popular technologies for developing the client side of the web project. Such bundle allows to extend product’s capabilities by simplifying the development of its features. Of course, that’s nothing ever perfect and these technologies are not an exception. In this article, I will show you how to avoid the challenges when using React+Redux and why you сan use Redux Thunk to streamline the development process.

JavaScript is a language with the asynchronous cycle of events.  It means that all the logic is executed in one thread, but the sequence of the executable code is not defined by default. For example.

fetch('https://jsonplaceholder.typicode.com/todos/2').then((resp) => {
   console.log('data is fetched ');
});
console.log('Next statement after fetch');

When executing the code described above, the following actions will happen:

  • Data request
  • Message output “Next statement after fetch”
  • Data fetch
  • Message output  “Data is fetched”

For those who haven’t dealt with asynchronous programming before, such flow of actions may seem weird, as the message “Next statement after fetch” is last in the list and had to be executed in the last turn as well.

This peculiarity also becomes an obstacle, as the events in Redux dispatch synchronously by default. Synchronous execution of the code leads to problems with the app that requires a lot of API requests. The diagram below shows the basic idea of Redux.

This is a linear cyclical flow of executing a certain logic. Such approach works well and is stable when this refers to executing the linear code on the client. But in most cases, working with JavaScript on the client involves work with the network and solving the queries for receiving or updating the certain data.

Each request is an asynchronous code with the callback. For this reason, when the complex client logic is implemented, it may cause some mess in code and, as a result, a number of potential errors.

Of course, it’s not what we want to achieve.

Solution

So how the programmer can simplify his/her life when it comes to the situation described above? They can do it by rendering the parts of the asynchronous logic from the components into the actions.

In such a way, the block “Action” from the diagram turns from the linear code into the set of logic that can contain branching and callbacks. Also, you can call the next actions or the whole cascade of actions if necessary. Less words, more actions. Let’s get straight to the examples.

Implementation

This solution is based on the Redux support of the Middleware conception. Middleware in Redux allows to conduct the processing or response to the event that was dispatched before the event will get into reducer and influence the state.

import { applyMiddleware } from "redux";

const store = createStore(
  reducers,
  initialState,
  applyMiddleware(
    middlewareOne,
    middlewareTwo
  )
);

In most cases, middleware is the function that processes the event and renders it to the next middleware or reducer. The functions of the middleware are similar to the chain of functions, each of which dispatches the action to the next function.

You can read more here. This information is enough to understand the main point of the solution described below.

Redux Thunk package

This is a middleware package for Redux that allows to write action creators that return function instead of the action object. In addition, the internal function receives two parameters dispatch and getState.

It allows to conduct a certain logic inside the action creator, analyze the current state and dispatch the action or a number of them. It depends on the logic and you can do it not only linearly but in the callback of some network request as well.

It gives a significant flexibility in building the project logic. Below, you can see how it looks in practice

In Redux

const action = (payload) => {
	return ({
		type: ‘some_action_type’,
		payload,
    });
};

dispatch(actionCreatorFn(payload));

With middleware redux-thunk

const actionCreatorFn = (payload) => (dispatch, getState)  => {
	// some internal logic
    dispatch({
		type: ‘some_action_type’,
		payload,
	});

	// some other internal logic
    if (condition)  {
		dispatch({ type: ‘other_action’ });
	} else {
		dispatch({ type: ‘third_action’ });
	}
};

dispatch(actionCreatorFn(payload));

In such a way, there can be come set of logic inside the action creator with the several dispatched actions. Also, the action can be dispatched in the callback of some data request. If nothing is dispatched, there will be no any error and the state will remain in the current state without any changes.

Advantages of this approach

  • Unloads the components from the logic
  • Reduces the need to import the Redux components (Store / Dispatch) into the components of React or the app’s logic
  • Simplifies the asynchronous code
  • Makes the project more simple and comprehensible

Connecting the middleware during the project initialization

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';

// Note: this API requires redux@>=3.1.0
const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

Now let’s imagine a real case where we can use the feature described above. For example, it can be the data upload that consists of the chain of the requests of the following structure:

Above is the chain of the requests, where the data from the previous request are used as the parameters for executing the next one. In addition, after the execution of the request  a branching can occur, where not one but several requests will be executed.

const initCurrentUser = () => (dispatch, getState) => {
 fetch('/user').then((response) => {
   const { currentUserId } = response;
   dispatch({
     type: 'set_current_user_id',
     payload: { currentUserId },
   });
   dispatch(getUserExtendedInfo(currentUserId));
   dispatch(getUserContactsList(currentUserId));
   dispatch(getUserInboxMessages(currentUserId));
 });
};

const getUserContactsList = (userId) => (dispatch, getState) => {
 fetch(`/user/${userId}/contacts`).then((response) => {
   const { contactsList } = response;
   dispatch({
     type: 'set_user_contacts',
     payload: { 
	   userId,
       contactsList,
     },
   });
 });
};

const getUserInboxMessages = (userId) => (dispatch, getState) => {
 fetch(`/user/${userId}/inbox`).then((response) => {
   const { inbox } = response;
   dispatch({
     type: 'set_user_inbox',
     payload: {
       userId,
       inbox,
     },
   });
 });
};

const getUserExtendedInfo = (userId) => (dispatch, getState) => {
 fetch(`/user/${userId}/info`).then((response) => {
   const { userInfo } = response;
   dispatch({
     type: 'set_user_info',
     payload: {
       userId,
       userInfo,
     },
   });
   dispatch(getArticleDetails(userInfo.lastArticleId));
 });
};

const getArticleDetails = (articleId) => (dispatch, gestState) => {
 fetch(`/article/${articleId}`).then((response) => {
   const { articleDetails } = response;
   dispatch({
     type: 'set_article_details',
     payload: {
       articleId,
       articleDetails,
     },
   });
   dispatch(getAuthorDetails(articleDetails.authorId));
 });

};

const getAuthorDetails = (authorId) => (dispatch, getState) => {
 fetch(`/author/${authorId}/details`).then((response) => {
   const { authorDetails } = response;
   dispatch({
     type: 'set_author_details',
     payload: {
       authorId,
       authorDetails,
     },
   });
 });
};

dispatch(initCurrentUser());

This code describes the cascade data upload (illustrated in the diagram above). Here you can see the opportunity to despatch the next action creators in the action creator or dispatch the actions that will influence the state and also execute the action dispatch separately in the callback (asynchronously). The technique, described above allows to significantly simplify development.

Redux thunk is a middleware that allows to unload the components from the logic and simplify the asynchronous code. Such approach is not compulsory but useful on the big projects when code complexity becomes a roadblock on your way to success.