How to use Redux and its benefits

Introduction
Redux is one of the most popular state management libraries and specifically for ReactJs. It helps to centralize all the component states of the application and provide the ability to track all of state changes over time.
In this tutorial, we will be creating a ReactJs app with both Redux and Redux toolkit to demonstrate the advantages/disadvantages of Redux toolkit over Redux.
This is the demo of this project.
Redux in a nutshell
GUI Reducers
+-------------+ +-------------+
| User | Dispatch | Update state|
| interactions ------------------------------>| base on |
| | Action | action type |
+-------------+ +--------------+ +-------------+
^ | +Type | |
| | +Payload | return |new states
update | +--------------+ |
front-end/GUI | |
| +------v-----+
| | |
| Notify front-end |store new |Store
<-------------------------------------|states |
| |
+------------+
Redux comprise of 3 main components: Action, Reducer, and Store (storing the states)
Action: an planed javascript object containing detail of an action. For example an action of pouring water into a bottle. Its detail will be:
{
type: "pour_water_into_bottle",
payload: {
used_hand: "right",
amount_of_water: "4 cup"
}
}
Reducer: a functions calculates and then return new state of our application. Follow the pouring water example above, the reducers will receive action from user with all the details and current state value and return the result:
Current/initial state:
application_state: {
water_in_bottle: "0 cup",
bottle_size: "4 cup",
bottle_is_full: "no"
}
After pouring action (new state) :
application_state: {
water_in_bottle: "4 cup",
bottle_size: "4 cup",
bottle_is_full: "yes"
}
Store : is where all the state (plain javascript object) and functions to trigger actions are stored. At this stage, the new state is updated and notify the front-end to re-render a new image to user (the bottle has more water and is full compare to the previous state is empty)
That was a simple explaination of how redux works.
The scaffolding directories
Presumably you have created React project using create-react-app
cli.
We are going to create 2 redux store using both Redux and Redux Toolkit, so this is the result structure after fully implementing the project and I am going to go through steps by steps shortly.
This is the structure of the project or you could find it at github link here
.
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
├── App.css
├── App.js
├── App.test.js
├── components
│ ├── CounterRedux
│ │ ├── counterRedux.js
│ │ └── index.js
│ └── CounterReduxToolkit
│ ├── counterReduxToolkit.js
│ └── index.js
├── index.css
├── index.js
├── logo.svg
├── redux
│ ├── counterRedux.js
│ ├── counterReduxToolkit.js
│ ├── storeRedux.js
│ └── storeReduxToolkit.js
├── reportWebVitals.js
└── setupTests.js
Counter component using Redux
Let begin with the Redux, we need to have add redux libraries:
1. Install some required libraries
$ npm install redux
$ npm install react-redux
2. Define action types
// create action alias
const ACTION_INCREASE = "ACTION_INCREASE";
const ACTION_DECREASE = "ACTION_DECREASE";
const ACTION_ASYNC_SAVE_STARTED = "ACTION_ASYNC_SAVE_STARTED";
const ACTION_ASYNC_SAVE_SUCCEED = "ACTION_ASYNC_SAVE_SUCCEED";
const ACTION_ASYNC_SAVE_FAIL = "ACTION_ASYNC_SAVE_FAIL";
You might wonder why we need these definitions.
Firstly, because we are going to use these constants in various places, so we need to define them only in 1 place. That makes the code management and refactoring easier later on.
Secondly, these definitions are used in the redux debug tool and it will show the values of these contants relatively to redux actions. Thus, the readability and clarity are important.
3. Create the actions
// create action and its payload
export const actionIncrease = (num = 1) => ({
type: ACTION_INCREASE,
payload: {
num
}
})
export const actionDecrease = (num = 1) => ({
type: ACTION_DECREASE,
payload: {
num
}
})
export const actionAsyncSaveStarted = (message) => ({
type: ACTION_ASYNC_SAVE_STARTED,
payload: {
message: message
}
})
export const actionAsyncSaveSucceed = (message) => ({
type: ACTION_ASYNC_SAVE_SUCCEED,
payload: {
message: message
}
})
export const actionAsyncSaveFail = (message) => ({
type: ACTION_ASYNC_SAVE_FAIL,
payload: {
error: message
}
})
export const actionSave = () => {
return (dispathObject) => {
dispathObject(actionAsyncSaveStarted("saving..."));
const fakeRequest = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("save succeeded")
// reject("save failed");
}, 2000);
})
return fakeRequest
.then(result => {
console.log("success", result)
dispathObject(actionAsyncSaveSucceed(result))
})
.catch(error => {
console.log("error", error)
dispathObject(actionAsyncSaveFail(error))
})
}
}
Notice the actionSave, we will discuss about it at the redux-thunk section.
4. Now the reducer
const initialState = {
number: 0,
savestate: ''
}
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_INCREASE:
return {...state, number: state.number + action.payload.num, savestate: "unsaved"}
case ACTION_DECREASE:
return {...state, number: state.number - action.payload.num, savestate: "unsaved"}
case ACTION_ASYNC_SAVE_STARTED:
return {...state, savestate: action.payload.message}
case ACTION_ASYNC_SAVE_SUCCEED:
return {...state, savestate: action.payload.message}
case ACTION_ASYNC_SAVE_FAIL:
return {...state, savestate: action.payload.error}
default: return state;
}
}
The reducer works as its decribed functions: receiving action and returning new state based on action payload and current state.
5. Creating the Store:
// redux/storeRedux.js
//- we named with the suffix "Redux" in order to compare with Redux Toolkit later
import { createStore } from 'redux';
import thunkMiddleware from 'redux-thunk';
import counterReducer from './counterRedux';
const rootReducers = combineReducers({
counterReducer: counterReducer
})
const store = createStore(
rootReducers
);
export default store;
Until here, we have completed our simple redux implementation, next will be using in an react component.
6. React component:
Since version v7.1.0, Redux Hooks were added and were recommended by Redux team over connect
API. So there are 2 basic hooks that we need:
useDispatch: to dispatch an actions
useSelector: to subscribe to changes of state. Whenever state is changed, this hook will trigger an update event of React and re-render page.
import React from 'react'
import { useDispatch, useSelector } from 'react-redux';
import { actionIncrease, actionDecrease } from '../../redux/counterRedux'
const CounterRedux = () => {
const number = useSelector(state => state.counterReducer.number);
const dispatch = useDispatch();
return (
<div>
<h4>Counter Redux</h4>
<button onClick={e => dispatch(actionIncrease())}>Add</button>
<button onClick={e => dispatch(actionDecrease())}>Subtract</button>
<h3>Result: <span>{number}</span></h3>
</div>
)
}
export default CounterRedux;
So now we have a running application but you may ask: what is the point of the whole boilerplate code above for just a simple functionality?
Well, I will list out some of the benefits:
Maintainability: when you application grows up to thousands of lines of code. Adding a new state or removing one might be a hassle but with Redux approach, it becomes easier. You only need to create new separated file of actions, reducers, and states then have components subscribe to new states.
Tractability: each action, there will be a brand new state object in the store, this will give us an ability to trace back every steps and actions previously and to reproduce whatever series of actions we want. This is a huge advantages of helping you debuging, testing, and fixing errors without repeating all the steps at the beginning over and over again.
7. Asynchronous requests - redux thunk
Not all actions of the application are synchronous. The application usually has async requests to interact with back end servers. Therefore Redux give us redux-thunk to handle the requests to servers.
Theoretically, we don’t need redux-thunk and the async call inside actionSave. We could call an async requests either in action functions or reducer functions. However it has disadvantages below:
Action: must always has dispatch object as the action’s argument. This means you have to inject the dispath object the action whenever testing either unit tests or integration tests.
Reducer: there will be side effects since the return state depends on the async request. This means you can not utilize the time travel feature of Redux development tool because the reducers are not pure functions anymore
So, the redux-thunk middleware came into place.
- redux-thunk requires action return a function with dispatchObject as its argument and when action is dispatched, the middleware will intercept the action and automatically passing the dispatch object, which is used inside the action function as we have:
...
export const actionSave = () => {
return (dispathObject) => {
dispathObject(actionAsyncSaveStarted("saving..."));
const fakeRequest = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("save succeeded")
// reject("save failed");
}, 2000);
})
return fakeRequest
.then(result => {
console.log("success", result)
dispathObject(actionAsyncSaveSucceed(result))
})
.catch(error => {
console.log("error", error)
dispathObject(actionAsyncSaveFail(error))
})
}
}
...
As a result, the action
function does not depend on dispathObject, and the reducers
are kept as pure functions. All the advantages of redux are preserved.
Install redux-thunk:
$ npm install redux-thunk
Add thunk to middleware of redux store
...
// add add dev tool to composer
const devtoolCompose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// apply middleware as an enhancer
const middlewareEnhancer = applyMiddleware(thunkMiddleware);
// compose all enhancers
const composeEnhancers = devtoolCompose(middlewareEnhancer);
...
Because the redux dev tool is not active by default, we have to manually initialize as the devtoolCompose
Finally, redux store provide a way to set default state at createStore
function:
....
// initialize state
const initialState = {
counterReducer: {
number: -1
}
}
export default createStore(
rootReducers,
initialState,
composeEnhancers
);
This is the complete store.js file:
import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import counterReducer from './counterRedux';
const rootReducers = combineReducers({
counterReducer: counterReducer
})
// add add dev tool to composer
const devtoolCompose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// apply middleware as an enhancer
const middlewareEnhancer = applyMiddleware(thunkMiddleware);
// compose all enhancers
const composeEnhancers = devtoolCompose(middlewareEnhancer);
// initialize state
const initialState = {
counterReducer: {
number: -1
}
}
export default createStore(
rootReducers,
initialState,
composeEnhancers
);
Conclusion
Redux is an useful state management library for all javascript frameworks, expecially for ReacJs.
Understand and get familiar with it is an advantage for any font-end developers on their daily life working tasks.