Redux

Jan 19, 2025

Redux is the state management library.

Redux is quite popular with React as React Redux. However, it’s not limited to React alone. Redux can be used with any other frameworks or even with plain JavaScript.

Today, it’s recommended to use Redux Toolkit rather than React-Redux. However, we are taking a different approach by using plain Redux to better understand its core fundamentals.

Create a folder named redux-app and navigate into it.

mkdir redux-app
cd redux-app

Generate a package.json file and then install the redux package.

npm init -y
npm i redux

Create a new file named index.js.

touch index.js

Open the file and import createStore from redux like this.

import { createStore } from 'redux';

createStore is used to create a Redux store. By calling the .createStore() method, we create a store that can be assigned to the store variable.

import { createStore } from 'redux';

const store = createStore();

The .createStore() method takes a reducer function as an argument. A reducer is simply a function that accepts state and action as parameters and returns a new state based on those two values.

Let’s define a reducer function called reducer that returns the state, and then pass the name of the reducer to the .createStore() method.

import { createStore } from 'redux';

const reducer = (state, action) => {
  return state;
};

const store = createStore(reducer);

We can optionally define a default value for the state, making sure that the reducer returns something instead of undefined.

import { createStore } from 'redux';

const reducer = (state = { amount: 0 }, action) => {
  return state;
};

const store = createStore(reducer);

With just a few lines of code, we’ve set up the Redux store. The store holds the state, and to get the state value, the .getState() method is used.

import { createStore } from 'redux';

const reducer = (state = { amount: 0 }, action) => {
  return state;
};

const store = createStore(reducer);

console.log(store.getState());

Let’s run the application using node index.js or node . command in the terminal.

$ node .
SyntaxError: Cannot use import statement outside a module

Running this command throws an error because we’re using the import...from syntax, which is not supported by default in Node.js. To make it work, we need to set the type to module in the package.json file.

{
  "name": "redux-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "redux": "^5.0.1"
  }
}

Now, let’s run the program, and it should print the state value, which will be an object like { amount: 0 }.

The reducer function is responsible for updating the state in the store. We need to define the logic within the reducer to handle state updates. Then outside of the reducer, we’ll pass a commands or instructions which may include optional data, to update the state value in the store.

Let me provide an example of how to update the state within the reducer.

import { createStore } from 'redux';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === 'increment') {
    return { amount: state.amount + 1 };
  }
  return state;
};

const store = createStore(reducer);

console.log(store.getState());

We added a condition to check if the action.type is the string “increment”, and if so, we increase the amount by 1 and return the updated state. There are a few important things to note about this logic.

  1. We should always return the state in the same shape it was originally defined. For example, if the state is an object, we must return an object (with updated or unchanged values based on the logic). For example, if the state is a number, we must return a number, and so on. This is why I’m returning { amount: state.amount + 1 }, as it maintains the shape of the state.
  2. We should never mutate the state directly. To avoid this in above example, we first access the existing value with state.amount, then add 1 to it, and assign the result back to amount, ensuring the state remains immutable.
  3. Within the reducer, we always have access to the current state value. That’s why we reference state.amount instead of just amount directly, as we need to access the state from the provided state object.
  4. Finally, we should always return the state, whether it has been updated or not. If no changes are made, simply return the existing state as it is.

To update the state in the store, we need to call the .dispatch() method, passing an action object such as { type: 'increment' }.

import { createStore } from 'redux';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === 'increment') {
    return { amount: state.amount + 1 };
  }
  return state;
};

const store = createStore(reducer);

store.dispatch({ type: 'increment' });

console.log(store.getState());

In Redux terminology, we refer to this as dispatching an action, since the argument passed to the .dispatch() method is an action. An action is essentially just a plain object. Inside the action, we have a type property, which is called the action type.

Now, running this program should update the amount in state by one.

node .
{ amount: 1 }

The argument that we passed to .dispatch() method is an action accepted as second argument of the reducer function. action.type is increment which is an if condition within reducer function. So, it updated the amount value in state and return it.

Instead of passing { type: 'increment' } directly to the .dispatch() function, we can define a function that returns this object and pass that function as an argument to .dispatch(). Let’s name this function increment.

import { createStore } from 'redux';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === 'increment') {
    return { amount: state.amount + 1 };
  }
  return state;
};

const store = createStore(reducer);

const increment = () => {
  return { type: 'increment' };
};

store.dispatch(increment());

console.log(store.getState());

Functions like these, which return actions, are called action creators in Redux. While they are simply functions that return plain objects, those plain objects represent actions in Redux, which is why we refer to these functions as action creators.

Currently, we’re repeating the action type string 'increment' twice — once in the reducer function and once in the action creator. This introduces the risk of errors due to typos. To avoid this, let’s define a constant INCREMENT with the value 'increment' and replace the string occurrences with this constant.

import { createStore } from 'redux';

const INCREMENT = 'increment';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  return state;
};

const store = createStore(reducer);

const increment = () => {
  return { type: INCREMENT };
};

store.dispatch(increment());

console.log(store.getState()); 

Running the program should produce the same output, but the code is now more maintainable (though not necessarily more readable!).

Next, let’s add support for decrementing the amount by one in this application. The dispatch process for decrementing will be the same as it is for incrementing.

import { createStore } from 'redux';

const INCREMENT = 'increment';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  return state;
};

const store = createStore(reducer);

const increment = () => {
  return { type: INCREMENT };
};

store.dispatch(increment());
store.dispatch(decrement());

console.log(store.getState());

However, we don’t have a decrement() action creator yet. Let’s go ahead and add it.

import { createStore } from 'redux';

const INCREMENT = 'increment';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  return state;
};

const store = createStore(reducer);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: 'decrement' };
};

store.dispatch(increment());
store.dispatch(decrement());

console.log(store.getState()); 

As we discussed earlier, using literal strings in multiple places can be error-prone. To avoid this, let’s create a DECREMENT constant to hold the 'decrement' string and use it in the action creator.

import { createStore } from 'redux';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  return state;
};

const store = createStore(reducer);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};

store.dispatch(increment());
store.dispatch(decrement());

console.log(store.getState());

Finally, let’s implement the logic in the reducer function to decrement the amount by 1.

import { createStore } from 'redux';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  return state;
};

const store = createStore(reducer);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};

store.dispatch(increment());
store.dispatch(decrement());

console.log(store.getState());

The application now supports the decrement functionality. Let’s run the program to confirm that it’s working as expected.

node .
{ amount: 0 }

It’s working! The initial value was 0, then we dispatched the increment action, which increased the amount by 1. After that, we dispatched the decrement action, reducing the amount by 1 and bringing it back to 0.

While printing the state before the increment, after the increment, and after the decrement would give us a clear view of the state updates, I have a better suggestion. Instead of printing line by line, we can subscribe to the store using the .subscribe() method. This method accepts a callback that is executed every time the state changes. By placing a console.log() inside this callback, we can automatically print the state after each update. Here’s the code:

import { createStore } from 'redux';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  return state;
};

const store = createStore(reducer);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};

**store.subscribe(() => {
  console.log(store.getState());
});**

store.dispatch(increment());
store.dispatch(decrement());

What we did here is, placed the console.log() line within callback function of subscribe method and re-positioned the call to subscribe method above the dispatch. Because, we first need to subscribe and then it will execute the callback after each dispatch.

Let’s run the program again.

node .
{ amount: 1 }
{ amount: 0 }

Now, the output is informative as after first dispatch the amout became 1 and then after second dispatch the amout become 0. If needed, then we print the initial state before the subscribe or I’ve better solution - use redux-logger package. redux-logger package is the middleware that will log the state update for us. Instead of me explaining how this middleware work, let see it in action!

Go ahead and install the redux-logger package.

npm i redux-logger

Import the logger from the redux-logger package.

import { createStore } from 'redux';
**import logger from 'redux-logger';**

const INCREMENT = 'increment';
const DECREMENT = 'decrement';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  return state;
};

const store = createStore(reducer);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};

store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(increment());
store.dispatch(decrement());

logger is the Redux middleware. So, we need to register it. To register the middleware we need to use applyMiddleware() function from redux. Whatever middleware that you want to register, pass it as an argument to it and applyMiddleware() register it. This applyMiddleware() function call should be the second argument of createStore() function.

Let’s do it step-by-step. First import applyMiddleware from the redux package.

import { createStore, **applyMiddleware** } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  return state;
};

const store = createStore(reducer);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};

store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(increment());
store.dispatch(decrement());

Next, applyMiddleware() function call should be the second argument of the createStore() function.

import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  return state;
};

const store = createStore(reducer, **applyMiddleware()**);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};

store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(increment());
store.dispatch(decrement());

Finally, pass the logger as an argument of applyMiddleware() function call. We need to pass logger.default as we are in Node application.

import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  return state;
};

const store = createStore(reducer, **applyMiddleware(logger.default)**);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};

store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(increment());
store.dispatch(decrement());

We can remove the store subscribe as this package internally do it for us.

import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  return state;
};

const store = createStore(reducer, applyMiddleware(logger.default));

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};

store.dispatch(increment());
store.dispatch(decrement());

Let’s run the program again.

node .
action increment @ 22:47:09.282
   prev state { amount: 0 }
   action     { type: 'increment' }
   next state { amount: 1 }
 action decrement @ 22:47:09.288
   prev state { amount: 1 }
   action     { type: 'decrement' }
   next state { amount: 0 }

Much better as it show all the need debug information including the previous state before the update, action that will update the state, the state after update, and time.


So far, we are not passing the input to the business logic. I mean take a look at the business logic. The logic is written within reducer function, we are only telling which one to execute. But, we are not passing anything that is directly affect the state value. Let’s consider the example, where we are interested to increment the amount by X or decrement the amount by Y where X and Y are the values that we will pass when we dispatch the action. For this, we need to pass the payload with the action in action creator and when we call the action creator, we will pass the exact values of this X and Y.

Previously, when adding decrement logic, we implemented it from how we want it to back on how it implemented as we first start from how we should dispatch the decrement action as if the code is available for us and then we added the needed code.

In current scenario, we will go in reverse order. We first add the needed code and then we will dispatch the incrementBy and decrementBy actions.

First we need INCREMENT_BY and DECREMENT_BY constants that will save the incrementBy and decrementBy string.

import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';
**const INCREMENT_BY = 'incrementBy';
const DECREMENT_BY = 'decrementBy';**

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  return state;
};

const store = createStore(reducer, applyMiddleware(logger.default));

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};

store.dispatch(increment());
store.dispatch(decrement());

Next, add the logic in the reducer function. The value that we will pass should be available in action and more specifically in action.payload.

import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';
const INCREMENT_BY = 'incrementBy';
const DECREMENT_BY = 'decrementBy';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  **if (action.type === INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }**
  return state;
};

const store = createStore(reducer, applyMiddleware(logger.default));

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};

store.dispatch(increment());
store.dispatch(decrement());

Let’s create the action creators - incrementBy and decrementBy. These action creators accept the values that we will set to payload next to the type in object as reducer function assume that action should contains amout value in payload.

import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';
const INCREMENT_BY = 'incrementBy';
const DECREMENT_BY = 'decrementBy';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const store = createStore(reducer, applyMiddleware(logger.default));

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};
**const incrementBy = (value) => {
  return { type: INCREMENT_BY, payload: value };
};
const decrementBy = (value) => {
  return { type: DECREMENT_BY, payload: value };
};**

store.dispatch(increment());
store.dispatch(decrement());

Finally, we can call these action creators by passing 50 and 20 as values respectively.

import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';
const INCREMENT_BY = 'incrementBy';
const DECREMENT_BY = 'decrementBy';

const reducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const store = createStore(reducer, applyMiddleware(logger.default));

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};
const incrementBy = (value) => {
  return { type: INCREMENT_BY, payload: value };
};
const decrementBy = (value) => {
  return { type: DECREMENT_BY, payload: value };
};

store.dispatch(increment());
store.dispatch(decrement());
**store.dispatch(incrementBy(50));
store.dispatch(decrementBy(20));**

Let’s run this program.

node .
 action increment @ 23:03:41.759
   prev state { amount: 0 }
   action     { type: 'increment' }
   next state { amount: 1 }
 action decrement @ 23:03:41.765
   prev state { amount: 1 }
   action     { type: 'decrement' }
   next state { amount: 0 }
 **action incrementBy @ 23:03:41.765
   prev state { amount: 0 }
   action     { type: 'incrementBy', payload: 50 }
   next state { amount: 50 }
 action decrementBy @ 23:03:41.765
   prev state { amount: 50 }
   action     { type: 'decrementBy', payload: 20 }
   next state { amount: 30 }**

Looks like the code for the incrementBy and decrementBy is working as state is updating correctly!


So far, we are only working with a single reducer to manage the amount. That is fine because we assume that we only have one functionality oriented around the amount. But, real-world application will have the many functionalities and we need to manage the state of them in store. In Redux, you can have one and only one store. So, what we need to do is, we need to combine the state of the different functionalities into single object and that object should be a single store.

Let’s expand our application and think of it as, an application that have two features - balance that manage the existing amount and another feature called the bonus that manages the points. To support this, we need to adjust the existing application by renaming the reducer function to balanceReducer.

import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';
const INCREMENT_BY = 'incrementBy';
const DECREMENT_BY = 'decrementBy';

const **balanceReducer** = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const store = createStore(**balanceReducer**, applyMiddleware(logger.default));

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};
const incrementBy = (value) => {
  return { type: INCREMENT_BY, payload: value };
};
const decrementBy = (value) => {
  return { type: DECREMENT_BY, payload: value };
};

store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(incrementBy(50));
store.dispatch(decrementBy(20));

balanceReducer name make more sense than just reducer because now application will have more than one reducer.

As mentioned previously, Redux can have only one store and because of that we are going to combine the reducer into single one. To do this, we need to use combineReducers() function from Redux. Let’s import it first.

import { createStore, applyMiddleware, **combineReducers** } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';
const INCREMENT_BY = 'incrementBy';
const DECREMENT_BY = 'decrementBy';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const store = createStore(balanceReducer, applyMiddleware(logger.default));

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};
const incrementBy = (value) => {
  return { type: INCREMENT_BY, payload: value };
};
const decrementBy = (value) => {
  return { type: DECREMENT_BY, payload: value };
};

store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(incrementBy(50));
store.dispatch(decrementBy(20));

Instead of directly passing balanceReducer, we’ll call a combineReducers() function and this function accepts an object with name that you want to use to work with reducer and value as name of reducer.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';
const INCREMENT_BY = 'incrementBy';
const DECREMENT_BY = 'decrementBy';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const store = createStore(**combineReducers({ balance: balanceReducer })**, applyMiddleware(logger.default));

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};
const incrementBy = (value) => {
  return { type: INCREMENT_BY, payload: value };
};
const decrementBy = (value) => {
  return { type: DECREMENT_BY, payload: value };
};

store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(incrementBy(50));
store.dispatch(decrementBy(20));

With these adjustments, let’s run the application. I want to show you something in store.

node .
action increment @ 12:46:12.315
   **prev state { balance: { amount: 0 } }**
   action     { type: 'increment' }
   **next state { balance: { amount: 1 } }**
 action decrement @ 12:46:12.323
   **prev state { balance: { amount: 1 } }**
   action     { type: 'decrement' }
   **next state { balance: { amount: 0 } }**
 action incrementBy @ 12:46:12.323
   **prev state { balance: { amount: 0 } }**
   action     { type: 'incrementBy', payload: 50 }
   **next state { balance: { amount: 50 } }**
 action decrementBy @ 12:46:12.324
   **prev state { balance: { amount: 50 } }**
   action     { type: 'decrementBy', payload: 20 }
   **next state { balance: { amount: 30 } }**

Notice, state no longer directly points to amount. Instead, it is balance.amount. Here, this balance comes from the name we gave inside the combineReducer() function. If you give the name x then state will be x.amount. I will not going to make that change. But, you should try to see it in the action!

Now, we are ready to create a reducer for the bonus functionality. In bonus, we will work with points and it also have the same functionalities - increment by 1, decrement by 1, increment by X points, and decrement by Y points. So, we can safely copy the balanceReducer function and rename the copy with bonusReducer. Plus, bonus works on points. So, we’ll also update the occurrence of amount in bonusReducer with points.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';
const INCREMENT_BY = 'incrementBy';
const DECREMENT_BY = 'decrementBy';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

**const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};**

const store = createStore(combineReducers({ x: balanceReducer }), applyMiddleware(logger.default));

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};
const incrementBy = (value) => {
  return { type: INCREMENT_BY, payload: value };
};
const decrementBy = (value) => {
  return { type: DECREMENT_BY, payload: value };
};

store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(incrementBy(50));
store.dispatch(decrementBy(20));

We now need to register this bonusReducer as bonus in combineReducers function.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';

const INCREMENT = 'increment';
const DECREMENT = 'decrement';
const INCREMENT_BY = 'incrementBy';
const DECREMENT_BY = 'decrementBy';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  **combineReducers({ balance: balanceReducer, bonus: bonusReducer }),**
  applyMiddleware(logger.default)
);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};
const incrementBy = (value) => {
  return { type: INCREMENT_BY, payload: value };
};
const decrementBy = (value) => {
  return { type: DECREMENT_BY, payload: value };
};

store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(incrementBy(50));
store.dispatch(decrementBy(20));

Let’s run the application and see what happen!

node .
action increment @ 13:29:07.599
   **prev state { balance: { amount: 0 }, bonus: { points: 0 } }**
   action     { type: 'increment' }
   **next state { balance: { amount: 1 }, bonus: { points: 1 } }**
 action decrement @ 13:29:07.605
   **prev state { balance: { amount: 1 }, bonus: { points: 1 } }**
   action     { type: 'decrement' }
   **next state { balance: { amount: 0 }, bonus: { points: 0 } }**
 action incrementBy @ 13:29:07.605
   **prev state { balance: { amount: 0 }, bonus: { points: 0 } }**
   action     { type: 'incrementBy', payload: 50 }
   **next state { balance: { amount: 50 }, bonus: { points: 50 } }**
 action decrementBy @ 13:29:07.606
   **prev state { balance: { amount: 50 }, bonus: { points: 50 } }**
   action     { type: 'decrementBy', payload: 20 }
   **next state { balance: { amount: 30 }, bonus: { points: 30 } }**

With the amount, points are also updated in same exact way. The reason is simple - when we dispatch the action, it’ll run in all the registered reducer and if condition within reducer functions match it’ll update that partition of the state. For example, if we dispatch the increment action, the condition for the increment action is present in both reducer functions. So, not only amount is going to be updated but also the points at the same time.

In some situations, you might want to run the reducers in this way. But, not in all the situations. The reason why it is happening is because of the same action type. We have the ‘increment’ action type in both reducers. If we change the action type, then it will only run when we explicitly dispatch that action. And that should be the case. Consider the example where you only want to increase the bonus by 1 point when amount in increment by ≥ 100.

Let’s change the action type then. First, we are going to define the constants like this.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';

**// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';**

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default)
);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};
const incrementBy = (value) => {
  return { type: INCREMENT_BY, payload: value };
};
const decrementBy = (value) => {
  return { type: DECREMENT_BY, payload: value };
};

store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(incrementBy(50));
store.dispatch(decrementBy(20));

The name of the constants are updated by prefixing the BALANCE_ in name and balance/ in value. Also, we did the same for bonus modules but prefixed appropriately. Actually, this is how the Redux suggest to name the action type - module/action_type. Usually, the constants are named after action_type. We prefixed it by the module because we’re writing everything in single file.

Next, we need to update the application to use these constants. So, let’s first update within both reducers.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === **BALANCE_INCREMENT**) {
    return { amount: state.amount + 1 };
  }
  if (action.type === **BALANCE_DECREMENT**) {
    return { amount: state.amount - 1 };
  }
  if (action.type === **BALANCE_INCREMENT_BY**) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === **BALANCE_DECREMENT_BY**) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === **BONUS_INCREMENT**) {
    return { points: state.points + 1 };
  }
  if (action.type === **BONUS_DECREMENT**) {
    return { points: state.points - 1 };
  }
  if (action.type === **BONUS_INCREMENT_BY**) {
    return { points: state.points + action.payload };
  }
  if (action.type === **BONUS_DECREMENT_BY**) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default)
);

const increment = () => {
  return { type: INCREMENT };
};
const decrement = () => {
  return { type: DECREMENT };
};
const incrementBy = (value) => {
  return { type: INCREMENT_BY, payload: value };
};
const decrementBy = (value) => {
  return { type: DECREMENT_BY, payload: value };
};

store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(incrementBy(50));
store.dispatch(decrementBy(20));

We need to update the existing function creators to use the new constants and prefix the name of the action creator functions as we need to create action creators for bonus module as well.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default)
);

// balance module
const **balanceIncrement** = () => {
  return { type: **BALANCE_INCREMENT** };
};
const **balanceDecrement** = () => {
  return { type: **BALANCE_DECREMENT** };
};
const **balanceIncrementBy** = (value) => {
  return { type: **BALANCE_INCREMENT_BY**, payload: value };
};
const **balanceDecrementBy** = (value) => {
  return { type: **BALANCE_DECREMENT_BY**, payload: value };
};

**// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};**

store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(incrementBy(50));
store.dispatch(decrementBy(20));

Finally, let’s dispatch these created action creators.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

**store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));**

**store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));**

Running this program should return following output.

node .
action balance/increment @ 16:42:35.187
   prev state { balance: { amount: 0 }, bonus: { points: 0 } }
   action     { type: 'balance/increment' }
   next state { balance: { amount: 1 }, bonus: { points: 0 } }
 action balance/decrement @ 16:42:35.194
   prev state { balance: { amount: 1 }, bonus: { points: 0 } }
   action     { type: 'balance/decrement' }
   next state { balance: { amount: 0 }, bonus: { points: 0 } }
 action balance/incrementBy @ 16:42:35.194
   prev state { balance: { amount: 0 }, bonus: { points: 0 } }
   action     { type: 'balance/incrementBy', payload: 50 }
   next state { balance: { amount: 50 }, bonus: { points: 0 } }
 action balance/decrementBy @ 16:42:35.194
   prev state { balance: { amount: 50 }, bonus: { points: 0 } }
   action     { type: 'balance/decrementBy', payload: 20 }
   next state { balance: { amount: 30 }, bonus: { points: 0 } }
 action bonus/increment @ 16:42:35.195
   prev state { balance: { amount: 30 }, bonus: { points: 0 } }
   action     { type: 'bonus/increment' }
   next state { balance: { amount: 30 }, bonus: { points: 1 } }
 action bonus/decrement @ 16:42:35.195
   prev state { balance: { amount: 30 }, bonus: { points: 1 } }
   action     { type: 'bonus/decrement' }
   next state { balance: { amount: 30 }, bonus: { points: 0 } }
 action bonus/incrementBy @ 16:42:35.195
   prev state { balance: { amount: 30 }, bonus: { points: 0 } }
   action     { type: 'bonus/incrementBy', payload: 50 }
   next state { balance: { amount: 30 }, bonus: { points: 50 } }
 action /bonus/decrementBy @ 16:42:35.196
   prev state { balance: { amount: 30 }, bonus: { points: 50 } }
   action     { type: '/bonus/decrementBy', payload: 20 }
   next state { balance: { amount: 30 }, bonus: { points: 30 } }

Based on the output we can safely says that the changes we made are now working and individual actions are running as action types are now unique across the application.


Let’s discuss the final topic of the Redux. Final in the sense of this article as there are many topics in the Redux that we haven’t covered in this article. So far, everything was synchronous - state is synchronous so as the update. What we returned from the store is synchronous so as the reducer logic. But, there will be scenarios where we want to write asynchronous logic and interested to manage it with Redux specially API call.

Consider the example where we want to fetch the balance details from the API. In order to do that, we first need to setup the back-end for the same. Lucky for us, it is quite easy.

First, create a db.json file.

touch db.json

Open this db.json file and write the following code.

{
  "accounts": [
    { "id": 1, "amount": 100 },
    { "id": 2, "amount": 200 }
  ],
  "bonuses": [
    { "id": 1, "points": 1 },
    { "id": 1, "points": 2 }
  ]
}

Nothing unknown - a sample JSON that has the amount for the given id with bonus points.

Now, open a new terminal and run the following command.

npx json-server db.json

The server or back-end API is now up and running on port 3000 and we have two end-points - /accounts and /bonuses. We can even fetch an individual account by passing id value as - /accounts/1 and it returns first account details.

Let’s install axios to fetch the data.

npm i axios

Let’s import the axios and call the end-point to fetch the account details within getAmount() function as follows.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
**import axios from 'axios';**

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

**const getAmount = async () => {
  const { data } = await axios.get('http://localhost:3000/accounts/1');
  console.log(data);
};
getAmount();**

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

Run the program and you should get the amound details at the end after Redux log.

node .
..
..
{ id: '1', amount: 100 }

Let’s add a new business logic that will fetch, save, and return the amount from the API into the store. We call it init.

Let’s first start by adding a constant INIT that saves the init string.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

**const INIT = 'init';**

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

const getAmount = async () => {
  const { data } = await axios.get('http://localhost:3000/accounts/1');
  console.log(data);
};
getAmount();

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

Next, let’s add a logic in amountReducer function.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  **if (action.type === INIT) {
    return { amount: action.payload };
  }**
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

const getAmount = async () => {
  const { data } = await axios.get('http://localhost:3000/accounts/1');
  console.log(data);
};
getAmount();

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

Finally, add the init action creator.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  if (action.type === INIT) {
    return { amount: action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

**const init = () => {
  return { type: INIT, payload: value };
};**

const getAmount = async () => {
  const { data } = await axios.get('http://localhost:3000/accounts/1');
  console.log(data);
};
getAmount();

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

The value should comes from the getAmount() function call. So, let’s adjust getAmount() function call to return data that we’ll save inside the value variable within init() action creator and use it in payload.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  if (action.type === INIT) {
    return { amount: action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

**const init = async () => {
  const value = await getAmount();
  return { type: INIT, payload: value };
};**

**const getAmount = async () => {
  const { data } = await axios.get('http://localhost:3000/accounts/1');
  return data;
};**

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

Finally, let’s dispatch the init.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  if (action.type === INIT) {
    return { amount: action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

const init = async () => {
  const value = await getAmount();
  return { type: INIT, payload: value };
};

const getAmount = async () => {
  const { data } = await axios.get('http://localhost:3000/accounts/1');
  return data;
};

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

store.dispatch(init());

Let’s run the program. I’m expecting an error and here it is.

node .
..
..
Error: Actions must be plain objects. 
Instead, the actual type was: 'Promise'. 
You may need to add middleware to your store 
setup to handle dispatching other values, such as 
'redux-thunk' to handle dispatching functions. 
See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware 
and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware 
for examples
..

Rudux complaining about the logic as Redux expect to have plain objects for the actions where as we passed Promise. Even further, Redux suggesting to use a middleware such as redux-thunk. Let’s install this package.

npm i redux-thunk

Let’s import the thunk and registered as middleare.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';
**import { thunk } from 'redux-thunk';**

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  if (action.type === INIT) {
    return { amount: action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default, **thunk**)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

const init = async () => {
  const value = await getAmount();
  return { type: INIT, payload: value };
};

const getAmount = async () => {
  const { data } = await axios.get('http://localhost:3000/accounts/1');
  return data;
};

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

store.dispatch(init());

Now, we need to do some adjustment around the init function. First, remove the API call from the function as action creator should not contains the API call and should only contains the plain object that should accepts value as an argument for the payload.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';
import { thunk } from 'redux-thunk';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  if (action.type === INIT) {
    return { amount: action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default, thunk)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

**const init = async (value) => {
  return { type: INIT, payload: value };
};**

const getAmount = async () => {
  const { data } = await axios.get('http://localhost:3000/accounts/1');
  return data;
};

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

store.dispatch(init());

We are going to dispatch the getAmount() function. As we are dispatching getAmount() function, we need to adjust what it return. When we are calling a function by dispatching it, the function should return a function that accept dispatch and getState functions as the arguments.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';
import { thunk } from 'redux-thunk';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  if (action.type === INIT) {
    return { amount: action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default, thunk)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

const init = async (value) => {
  return { type: INIT, payload: value };
};

**const getAmount = () => {
  return async (dispatch, getState) => {
    const { data } = await axios.get('http://localhost:3000/accounts/1');
    return data;
  };
};**

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

**store.dispatch(getAmount());**

I’ve also adjusted async/await keywords as return function is async now.

Finally, dispatch the init action by passing the return value from the data as data.amount.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';
import { thunk } from 'redux-thunk';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  if (action.type === INIT) {
    return { amount: action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default, thunk)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

const init = (value) => {
  return { type: INIT, payload: value };
};

const getAmount = () => {
  return async (dispatch, getState) => {
    const { data } = await axios.get('http://localhost:3000/accounts/1');

    **dispatch(init(data.amount));**
  };
};

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

store.dispatch(getAmount());

This dispatch is available as an argument from the function and this dispatch is same as the store.dispatch() function.

Now, run the program.

node .
action balance/increment @ 17:16:04.662
   prev state { balance: { amount: 0 }, bonus: { points: 0 } }
   action     { type: 'balance/increment' }
   next state { balance: { amount: 1 }, bonus: { points: 0 } }
 action balance/decrement @ 17:16:04.665
   prev state { balance: { amount: 1 }, bonus: { points: 0 } }
   action     { type: 'balance/decrement' }
   next state { balance: { amount: 0 }, bonus: { points: 0 } }
 action balance/incrementBy @ 17:16:04.666
   prev state { balance: { amount: 0 }, bonus: { points: 0 } }
   action     { type: 'balance/incrementBy', payload: 50 }
   next state { balance: { amount: 50 }, bonus: { points: 0 } }
 action balance/decrementBy @ 17:16:04.666
   prev state { balance: { amount: 50 }, bonus: { points: 0 } }
   action     { type: 'balance/decrementBy', payload: 20 }
   next state { balance: { amount: 30 }, bonus: { points: 0 } }
 action bonus/increment @ 17:16:04.666
   prev state { balance: { amount: 30 }, bonus: { points: 0 } }
   action     { type: 'bonus/increment' }
   next state { balance: { amount: 30 }, bonus: { points: 1 } }
 action bonus/decrement @ 17:16:04.667
   prev state { balance: { amount: 30 }, bonus: { points: 1 } }
   action     { type: 'bonus/decrement' }
   next state { balance: { amount: 30 }, bonus: { points: 0 } }
 action bonus/incrementBy @ 17:16:04.667
   prev state { balance: { amount: 30 }, bonus: { points: 0 } }
   action     { type: 'bonus/incrementBy', payload: 50 }
   next state { balance: { amount: 30 }, bonus: { points: 50 } }
 action /bonus/decrementBy @ 17:16:04.667
   prev state { balance: { amount: 30 }, bonus: { points: 50 } }
   action     { type: '/bonus/decrementBy', payload: 20 }
   next state { balance: { amount: 30 }, bonus: { points: 30 } }
 **action undefined @ 17:16:04.667
   prev state { balance: { amount: 30 }, bonus: { points: 30 } }
   action     [AsyncFunction (anonymous)]
   next state { balance: { amount: 30 }, bonus: { points: 30 } }**
 **action init @ 17:16:04.678
   prev state { balance: { amount: 30 }, bonus: { points: 30 } }
   action     { type: 'init', payload: 100 }
   next state { balance: { amount: 100 }, bonus: { points: 30 } }**

As you see from the output that the API call is working and we are able to successfully pass the payload as an amount from the API result to the store in init action.

Two actions were dispatched - first one was from the store.dispatch() function and second was the dispatch() call and that is totally correct and fine.

Going further as we are depending on the API call. So, it might possible that we might not get the result or it take a long time to return result. In such a case, we need to hande these scenarios. To handle these scenarios, we need to save the appropriate state within store. At the end, the API call can be in three states - pending, fulfilled, and rejected. So, let’s add three strings for the same.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';
import { thunk } from 'redux-thunk';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';
**const INIT_PENDING = 'amount/pending';
const INIT_FULFILLED = 'amount/fulfilled';
const INIT_REJECTED = 'amount/rejected';**

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  if (action.type === INIT) {
    return { amount: action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default, thunk)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

const init = (value) => {
  return { type: INIT, payload: value };
};

const getAmount = () => {
  return async (dispatch, getState) => {
    const { data } = await axios.get('http://localhost:3000/accounts/1');

    dispatch(init(data.amount));
  };
};

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

store.dispatch(getAmount());

Let’s add needed action creators.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';
import { thunk } from 'redux-thunk';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';
const INIT_PENDING = 'amount/pending';
const INIT_FULFILLED = 'amount/fulfilled';
const INIT_REJECTED = 'amount/rejected';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  if (action.type === INIT) {
    return { amount: action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default, thunk)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

const init = (value) => {
  return { type: INIT, payload: value };
};
**const initPending = () => {
  return { type: INIT_PENDING };
};
const initFulFilled = () => {
  return { type: INIT_FULFILLED };
};
const initRejected = () => {
  return { type: INIT_REJECTED };
};**

const getAmount = () => {
  return async (dispatch, getState) => {
    const { data } = await axios.get('http://localhost:3000/accounts/1');

    dispatch(init(data.amount));
  };
};

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

store.dispatch(getAmount());

Now, we need to adjust the getAmount() function. Before the API call we’ll dispatch the pending action, after the API call, we’ll dispatch the fulfilled action, and finally, the API call should be wrapped within try/catch block so that in catch() block we can add dispatch the rejected action.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';
import { thunk } from 'redux-thunk';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';
const INIT_PENDING = 'amount/pending';
const INIT_FULFILLED = 'amount/fulfilled';
const INIT_REJECTED = 'amount/rejected';

const balanceReducer = (state = { amount: 0 }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  if (action.type === INIT) {
    return { amount: action.payload };
  }
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default, thunk)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

const init = (value) => {
  return { type: INIT, payload: value };
};
const initPending = () => {
  return { type: INIT_PENDING };
};
const initFulFilled = () => {
  return { type: INIT_FULFILLED };
};
const initRejected = () => {
  return { type: INIT_REJECTED };
};

**const getAmount = () => {
  return async (dispatch, getState) => {
    try {
      dispatch(initPending());
      const { data } = await axios.get('http://localhost:3000/accounts/1');
      dispatch(initFulFilled());

      dispatch(init(data.amount));
    } catch (error) {
      dispatch(initRejected());
    }
  };
};**

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

store.dispatch(getAmount());

Now, run the application.

node .
 action balance/increment @ 17:25:40.002
   prev state { balance: { amount: 0 }, bonus: { points: 0 } }
   action     { type: 'balance/increment' }
   next state { balance: { amount: 1 }, bonus: { points: 0 } }
 action balance/decrement @ 17:25:40.006
   prev state { balance: { amount: 1 }, bonus: { points: 0 } }
   action     { type: 'balance/decrement' }
   next state { balance: { amount: 0 }, bonus: { points: 0 } }
 action balance/incrementBy @ 17:25:40.006
   prev state { balance: { amount: 0 }, bonus: { points: 0 } }
   action     { type: 'balance/incrementBy', payload: 50 }
   next state { balance: { amount: 50 }, bonus: { points: 0 } }
 action balance/decrementBy @ 17:25:40.007
   prev state { balance: { amount: 50 }, bonus: { points: 0 } }
   action     { type: 'balance/decrementBy', payload: 20 }
   next state { balance: { amount: 30 }, bonus: { points: 0 } }
 action bonus/increment @ 17:25:40.007
   prev state { balance: { amount: 30 }, bonus: { points: 0 } }
   action     { type: 'bonus/increment' }
   next state { balance: { amount: 30 }, bonus: { points: 1 } }
 action bonus/decrement @ 17:25:40.008
   prev state { balance: { amount: 30 }, bonus: { points: 1 } }
   action     { type: 'bonus/decrement' }
   next state { balance: { amount: 30 }, bonus: { points: 0 } }
 action bonus/incrementBy @ 17:25:40.008
   prev state { balance: { amount: 30 }, bonus: { points: 0 } }
   action     { type: 'bonus/incrementBy', payload: 50 }
   next state { balance: { amount: 30 }, bonus: { points: 50 } }
 action /bonus/decrementBy @ 17:25:40.008
   prev state { balance: { amount: 30 }, bonus: { points: 50 } }
   action     { type: '/bonus/decrementBy', payload: 20 }
   next state { balance: { amount: 30 }, bonus: { points: 30 } }
 **action undefined @ 17:25:40.008
   prev state { balance: { amount: 30 }, bonus: { points: 30 } }
   action     [AsyncFunction (anonymous)]
   next state { balance: { amount: 30 }, bonus: { points: 30 } }
 action amount/pending @ 17:25:40.008
   prev state { balance: { amount: 30 }, bonus: { points: 30 } }
   action     { type: 'amount/pending' }
   next state { balance: { amount: 30 }, bonus: { points: 30 } }
 action amount/fulfilled @ 17:25:40.022
   prev state { balance: { amount: 30 }, bonus: { points: 30 } }
   action     { type: 'amount/fulfilled' }
   next state { balance: { amount: 30 }, bonus: { points: 30 } }
 action init @ 17:25:40.022
   prev state { balance: { amount: 30 }, bonus: { points: 30 } }
   action     { type: 'init', payload: 100 }
   next state { balance: { amount: 100 }, bonus: { points: 30 }** }

As you can see from the output that it first started with async dispatch, then it goes into the pending state as we’re about to send an API, then we get the API response and resule is fulfilled. Finally, we get the init action to save the result.

We do not have the business logic for these actions. We can easily add by adjusting the initial state to have the isLoading, isError, and so on.

import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import axios from 'axios';
import { thunk } from 'redux-thunk';

// balance module
const BALANCE_INCREMENT = 'balance/increment';
const BALANCE_DECREMENT = 'balance/decrement';
const BALANCE_INCREMENT_BY = 'balance/incrementBy';
const BALANCE_DECREMENT_BY = 'balance/decrementBy';

// bonus module
const BONUS_INCREMENT = 'bonus/increment';
const BONUS_DECREMENT = 'bonus/decrement';
const BONUS_INCREMENT_BY = 'bonus/incrementBy';
const BONUS_DECREMENT_BY = '/bonus/decrementBy';

const INIT = 'init';
const INIT_PENDING = 'amount/pending';
const INIT_FULFILLED = 'amount/fulfilled';
const INIT_REJECTED = 'amount/rejected';

const balanceReducer = (state = { amount: 0**, isLoading: false, isError: false** }, action) => {
  if (action.type === BALANCE_INCREMENT) {
    return { amount: state.amount + 1 };
  }
  if (action.type === BALANCE_DECREMENT) {
    return { amount: state.amount - 1 };
  }
  if (action.type === BALANCE_INCREMENT_BY) {
    return { amount: state.amount + action.payload };
  }
  if (action.type === BALANCE_DECREMENT_BY) {
    return { amount: state.amount - action.payload };
  }
  **if (action.type === INIT) {
    return { amount: action.payload, isLoading: false, isError: false };
  }
  if (action.type === INIT_PENDING) {
    return { ...state, isLoading: true };
  }
  if (action.type === INIT_REJECTED) {
    return { ...state, isLoading: false, isError: true };
  }**
  return state;
};

const bonusReducer = (state = { points: 0 }, action) => {
  if (action.type === BONUS_INCREMENT) {
    return { points: state.points + 1 };
  }
  if (action.type === BONUS_DECREMENT) {
    return { points: state.points - 1 };
  }
  if (action.type === BONUS_INCREMENT_BY) {
    return { points: state.points + action.payload };
  }
  if (action.type === BONUS_DECREMENT_BY) {
    return { points: state.points - action.payload };
  }
  return state;
};

const store = createStore(
  combineReducers({ balance: balanceReducer, bonus: bonusReducer }),
  applyMiddleware(logger.default, thunk)
);

// balance module
const balanceIncrement = () => {
  return { type: BALANCE_INCREMENT };
};
const balanceDecrement = () => {
  return { type: BALANCE_DECREMENT };
};
const balanceIncrementBy = (value) => {
  return { type: BALANCE_INCREMENT_BY, payload: value };
};
const balanceDecrementBy = (value) => {
  return { type: BALANCE_DECREMENT_BY, payload: value };
};

// bonus module
const bonusIncrement = () => {
  return { type: BONUS_INCREMENT };
};
const bonusDecrement = () => {
  return { type: BONUS_DECREMENT };
};
const bonusIncrementBy = (value) => {
  return { type: BONUS_INCREMENT_BY, payload: value };
};
const bonusDecrementBy = (value) => {
  return { type: BONUS_DECREMENT_BY, payload: value };
};

const init = (value) => {
  return { type: INIT, payload: value };
};
const initPending = () => {
  return { type: INIT_PENDING };
};
const initFulFilled = () => {
  return { type: INIT_FULFILLED };
};
const initRejected = () => {
  return { type: INIT_REJECTED };
};

const getAmount = () => {
  return async (dispatch, getState) => {
    try {
      dispatch(initPending());
      const { data } = await axios.get('http://localhost:3000/accounts/1');
      dispatch(initFulFilled());

      dispatch(init(data.amount));
    } catch (error) {
      dispatch(initRejected());
    }
  };
};

store.dispatch(balanceIncrement());
store.dispatch(balanceDecrement());
store.dispatch(balanceIncrementBy(50));
store.dispatch(balanceDecrementBy(20));

store.dispatch(bonusIncrement());
store.dispatch(bonusDecrement());
store.dispatch(bonusIncrementBy(50));
store.dispatch(bonusDecrementBy(20));

store.dispatch(getAmount());

Here is the output.

node .
 action balance/increment @ 17:35:15.279
   prev state {
    balance: { amount: 0, isLoading: false, isError: false },
    bonus: { points: 0 }
  }
   action     { type: 'balance/increment' }
   next state { balance: { amount: 1 }, bonus: { points: 0 } }
 action balance/decrement @ 17:35:15.282
   prev state { balance: { amount: 1 }, bonus: { points: 0 } }
   action     { type: 'balance/decrement' }
   next state { balance: { amount: 0 }, bonus: { points: 0 } }
 action balance/incrementBy @ 17:35:15.282
   prev state { balance: { amount: 0 }, bonus: { points: 0 } }
   action     { type: 'balance/incrementBy', payload: 50 }
   next state { balance: { amount: 50 }, bonus: { points: 0 } }
 action balance/decrementBy @ 17:35:15.283
   prev state { balance: { amount: 50 }, bonus: { points: 0 } }
   action     { type: 'balance/decrementBy', payload: 20 }
   next state { balance: { amount: 30 }, bonus: { points: 0 } }
 action bonus/increment @ 17:35:15.283
   prev state { balance: { amount: 30 }, bonus: { points: 0 } }
   action     { type: 'bonus/increment' }
   next state { balance: { amount: 30 }, bonus: { points: 1 } }
 action bonus/decrement @ 17:35:15.283
   prev state { balance: { amount: 30 }, bonus: { points: 1 } }
   action     { type: 'bonus/decrement' }
   next state { balance: { amount: 30 }, bonus: { points: 0 } }
 action bonus/incrementBy @ 17:35:15.283
   prev state { balance: { amount: 30 }, bonus: { points: 0 } }
   action     { type: 'bonus/incrementBy', payload: 50 }
   next state { balance: { amount: 30 }, bonus: { points: 50 } }
 action /bonus/decrementBy @ 17:35:15.284
   prev state { balance: { amount: 30 }, bonus: { points: 50 } }
   action     { type: '/bonus/decrementBy', payload: 20 }
   next state { balance: { amount: 30 }, bonus: { points: 30 } }
 action undefined @ 17:35:15.284
   prev state { balance: { amount: 30 }, bonus: { points: 30 } }
   action     [AsyncFunction (anonymous)]
   next state { balance: { amount: 30 }, bonus: { points: 30 } }
 **action amount/pending @ 17:35:15.284
   prev state { balance: { amount: 30 }, bonus: { points: 30 } }
   action     { type: 'amount/pending' }
   next state { balance: { amount: 30, isLoading: true }, bonus: { points: 30 } }
 action amount/fulfilled @ 17:35:15.294
   prev state { balance: { amount: 30, isLoading: true }, bonus: { points: 30 } }
   action     { type: 'amount/fulfilled' }
   next state { balance: { amount: 30, isLoading: true }, bonus: { points: 30 } }
 action init @ 17:35:15.294
   prev state { balance: { amount: 30, isLoading: true }, bonus: { points: 30 } }
   action     { type: 'init', payload: 100 }
   next state {
    balance: { amount: 100, isLoading: false, isError: false },
    bonus: { points: 30 }
  }**
Tags: redux react