Redux(with React) Simplified

An attempt(calling this an attempt because I was scared and confused as heck when I first got into Redux, but things got easier over time with experience and practice) to make novice React Developers understand working with Redux for global state management

Sayan Sarkar
10 min readNov 11, 2020

This article assumes familiarity with JavaScript(ES5 and ES6+) concepts like Arrow functions, map, rest & spread operators, etc on the reader’s end. It also assumes basic familiarity with ReactJS concepts on the reader’s end like JSX, Class Based Components, Component States, PropTypes, Lifecycle methods like componentDidMount(), componentWillReceiveProps() etc. Also, REST API basic understanding is assumed on the part of the reader.

What is Redux?

Redux, essentially, is a third party library, used mostly in conjunction with ReactJS ,which helps us to manage a global state for an application. In larger ReactJS apps, Redux is not only an optimal solution, but often a necessary process. According to Redux official documentation :

Redux is a predictable state container for JavaScript apps

What the above quote means is that Redux helps us to manage a global store which contains a state which can be used throughout the entire application by being available to all components in the application.

Redux Workflow

Redux Workflow Diagram

The above diagram demonstrates the workflow of a React app when Redux is used. Let’s go through each of the above mentioned entities:

  1. Store: The Redux Store, as the name suggests, stores the global application state. It passes this state to the View
  2. View: View is basically the component which is being displayed in the DOM or being imported by another component currently being displayed in the DOM.
  3. Actions: In the View, on events like button click on a form submit, or mouse hover on a menu, certain Action Creators are triggered which in turn dispatch some Actions (like fetch data from API, login to a server, logout from a server, etc) to the Store.
  4. Reducers: Reducers are basically pure functions that specify how the application state should change in response to the action which was dispatched(as mentioned in the previous step). Reducers respond with the new(updated) state in the store.

NOTE: State in React is immutable, so part of the state can’t be modified. Thus, reducers take the entire previous state and do the required modifications to it and return the new state which gets replaced in the store as the new state.

Understanding Redux with React: Hands-on

We create a react app using create-react-app .

npx create-react-app redux-example
cd my-app
npm start

We install certain dependencies needed to implement redux along with axios for making AJAX requests:

npm i --save redux react-redux redux-thunk axios

For an API to make request to, we use JSONPlaceholder. The directory structure of our app(excluding node_modules) is as follows:

redux-example
├── package.json
├── package-lock.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── README.md
└── src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── reportWebVitals.js
├── setupTests.js

All of the above is basic boilerplate stuff. We will be primarily focusing on a simple task i.e fetching all the posts from the JSONPlaceholder API.

We segregate our components into a separate directory in the root level. Our one and only component will be Posts.js which basically serves the purpose of calling the JSONPlaceholder API to fetch all posts in it’s componentDidMount() method.

Let’s setup Redux

I’ll be clear from the very beginning. There is no one correct way to set redux up. The way I’m showing, I find it to be the easiest. You might or might not follow the same, but in my personal opinion, this method should be the least cumbersome.

Let’s create a directory at the root(src/) level named store under which there’ll be 2 sub directories. actions and reducers. Both will have a file named posts.js. The actions folder will have 2 more files named actionTypes.js and index.js . The file structure is as follows:

store/
├── actions
│ ├── actionTypes.js
│ ├── index.js
│ └── posts.js
└── reducers
└── posts.js

Actions

Let’s take a look at all the action files.

src/store/actions/actionTypes.js:

export const FETCH_POSTS = 'FETCH_POSTS';

The actionTypes.js might seem like extra work for just one line of code but this will be effective when number of actions are more. So we can simply add all the action types of those actions in one single file and export them totally.

src/store/actions/posts.js:

import * as actionTypes from './actionTypes';
import axios from 'axios';
const fetchAllPosts = posts => {
return {
type: actionTypes.FETCH_POSTS,
payload: posts.data
};
};
export const fetchPosts = () => dispatch => {
console.log('posts action');
axios.get('https://jsonplaceholder.typicode.com/posts')
.then(posts => {
dispatch(fetchAllPosts(posts));
});
}

Let’s explain the above code step by step:

  1. The fetchPosts method is responsible for dispatching the action which makes an API Call to the JSONPlaceHolder endpoint, to fetch all posts.
  2. On getting a response from the API, the posts received as response are dispatched to the posts reducer via the fetchAllPosts method.
  3. The fetchAllPosts method returns the data received as API response in a key named payload, along with the action type of FETCH_POSTS, to the fetchPosts method from where it is dispatched to the reducer.
  4. The fetchAllPosts method can be bypassed in this case, but such segregation of dispatchable actions becomes essential in case there are multiple actions which need to be dispatched to the reducer from within the same method. So, it’s a good practice to follow this approach.

src/store/index.js:

export {fetchPosts} from './posts';

The above index.js is totally optional and you can skip this if you want. Personally, I like this way of importing actions.

Reducers

The Reducer is the place which stores the initial state of the app and updates it based on the action dispatched. So it conditionally updates the state based on the action dispatched, each update being different for different action . This is generally achieved with the help of a switch case but an if-else block could achieve the same. Let’s look at the code for the reducer.

src/store/reducers/posts.js

import * as actionTypes from '../actions/actionTypes';const initialState = {
items: []
};
const fetchPosts = (state, action) => {
return {
...state,
items: action.payload
}
}
const reducer = (state = initialState, action) => {
switch(action.type) {
case actionTypes.FETCH_POSTS: return fetchPosts(state, action)
default: return state
}
};
export default reducer;

Let’s analyze the above code, step by step:

  1. The initialState , as the name suggests, is the initial state of the app. In this case, it contains empty array named items which will store the fetched posts.
  2. The reducer method takes in 2 arguments: the initial state, and the action which was dispatched to the reducer. Based on the type of the action received( action.type in the switch case which is obtained from type in the return block of fetchAllPosts method of src/store/actions/posts.js) FETCH_POSTS in this case, a method(fetchPosts) is called which updates the state and returns it to the view component.
  3. The fetchPosts method in the reducer takes in 2 parameters: the previous state and the action object. It takes the payload from the received action and updates it to the items array of the state. This state is returned to the component which triggered the event the action responsible for state updation.
  4. Again for other scenarios,like adding a new post, the state has to be updated differently. All we need to do in that case is to add an extra case to the swtich statement

The above code can be optimized with a minor tweak. Since the state update is essentially copying the previous state and adding the changes to the previous state to make a new state, this will be same for all the reducer methods. So repetitively writing the same 4–5 lines of code is redundant. What I like to do is to create a directory at the root(src/) level named shared and like to name the file in it as util.js and create a utility method named updateObject.

src/shared/util.js

export const updateObject = (oldObject, updatedProperties)=>{
return{
...oldObject,
...updatedProperties
}
}

This helps our reducer code to be reduced by a lot. So the optimized src/store/reducers/posts.js is as follows:

import * as actionTypes from '../actions/actionTypes';
import {updateObject} from '../../shared/util';
const initialState = {
items: []
};
const fetchPosts = (state, action) => updateObject(state, {items: action.payload})const reducer = (state = initialState, action) => {
switch(action.type) {
case actionTypes.FETCH_POSTS: return fetchPosts(state, action)
default: return state
}
};
export default reducer;

This utility method can now be used not just inside the reducers, but anywhere in the app.

Connecting everything

Our Actions and Reducers have been setup completely. The question that arises is that how do we connect these actions and reducers to the components. Again, as mentioned in the beginning of the Hands-on section, there is no one correct way to establish this connection. The way I do it is in the index.js as it provides a centralised location for establishing the connection.

src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { compose, applyMiddleware, createStore, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import postsReducer from './store/reducers/posts';
const composeEnhancers = process.env.NODE_ENV === 'development' ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : null || compose;const rootReducer = combineReducers({
posts: postsReducer
});
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));const app = (<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)
ReactDOM.render( app, document.getElementById('root'));// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Before analysis of the code, let’s know the meaning of some of the entities used in the above code:

  1. Store: A store holds the whole state of your application. The only way to change the state inside it is to dispatch an action on it. To create a store, we need to pass our root reducer function to createStore
  2. Store creator: A store creator is a function that creates a redux store.
  3. createStore: It’s a store creator that creates a Redux store that holds the complete state tree of our app. There should only be a single store in our app. createStore takes 2 arguments(also there’s a 3rd optional one), namely the root reducer function and the enhancer list
  4. rootReducer: It basically is a function which is a culmination of all reducers in our app under a single roof, combined using the combineReducers method.
  5. Store enhancers: A store enhancer is a higher-order function that composes a store creator to return a new, enhanced store creator
  6. compose: It’s a functional component utility, included in Redux as a convenience method, which composes functions from left to right. It can be used to apply several store enhancers in a row.
  7. Middleware: A Middleware is the suggested way to extend Redux with custom functionality. Middleware lets us wrap the store’s dispatch method. The key feature of middleware is that it is composable. Multiple middleware can be combined together independent of each other. The most common use case for middleware is to support asynchronous actions without much boilerplate code or a dependency on a library.
  8. applyMiddleware: A utility method to apply all the middlewares in the app.
  9. thunk: A redux middleware for executing asynchronous tasks like AJAX requests and also, complex synchronous tasks like constant access to the store
  10. Provider: The <Provider /> makes the Redux store available to any nested components that have been wrapped in the connect() function. All we need to do is to wrap the main <App /> component in the <Provider /> with an initial value of store

Code Analysis

const composeEnhancers = process.env.NODE_ENV === 'development' ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : null || compose;

We are checking for production environment or development environment. In case of a development environment, we would want the app to have Redux Devtools as an enhancer, else we would use compose as an enhancer.

const rootReducer = combineReducers({
posts: postsReducer
});

As mentioned earlier, this helps to combine multiple reducers(one in case of our app) and pass it as a single reducer function

const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));

This sets up the redux store by passing the rootReducer and composeEnhancers as arguments to createStore. The composeEnahncers in turn takes the applyMiddleware method as argument with thunk being the only middleware being used in our app.

<Provider store={store}>
<App />
</Provider>

The above code snippet shows how the <Provider /> with an initial value for store wraps the App component.

Triggering an action from component

src/components/Posts.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {connect } from 'react-redux';
import * as actions from '../store/actions';class Posts extends Component { componentDidMount() {
this.props.FetchPosts()
}
componentWillReceiveProps(nextProps) { if(nextProps.newPost) {
this.props.Posts.unshift(nextProps.newPost);
}
}
render() { const postItems = this.props.Posts.map( post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
));
return (
<div>
<h1>Posts</h1>
{ postItems }
</div>
)
}
}
Posts.propTypes = {
Posts: PropTypes.array.isRequired,
FetchPosts: PropTypes.func.isRequired,
newPost: PropTypes.object
}
const mapStateToProps = state => {
return {
Posts: state.posts.items
}
};
const mapDispatchToProps = dispatch => {
return {
FetchPosts: () => dispatch(actions.fetchPosts())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Posts)

Let’s analyze the above mentioned code:

  1. connect(): The connect() function connects a React component to a Redux store. It takes in 2 arguments, mapStateToProps and mapDispatchToProps
  2. mapStateToProps: mapStateToProps is used for selecting the part of the state from the store that the connected component needs. It’s called everytime the store state changes. In this case, the posts in state.posts.items of mapStateToProps refers to posts reducer inside rootReducer of src/index.js. This property is mapped to Posts in src/components/Posts.js and can be accessed using this.props.Posts.
  3. mapDispatchToProps: This is somewhat similar to mapStateToProps. mapDispatchToProps is used for dispatching actions to the store. dispatch is a function of the Redux store. Wecall dispatch to dispatch an action. This is the only way to trigger a state change. The actions.fetchPosts() comes from fetchPosts() action of src/store/actions/posts.js and is mapped to FetchPosts in src/components/Posts.js and can be accessed using this.props.FetchPosts as shown in the componentDidMount() .

Final Step

The last step is a rather simple one where we simply import the Posts Component inside src/App.js and use it to complete the application which fetches all Posts from the JSONPlaceholder API.

Conclusion

This seemed like a huge setup just for the sake of fetching posts from an API and displaying it. But as I said before, as the features increase in an application, Redux becomes not only convenient, but necessary. I believe this template of Redux Boilerplate code was comparatively easier to understand than the other tutorials you find online. Once the initial setup is done, adding features becomes a lot more convenient and easy. I would like to have your honest feedback of this blog. If you like it, do share it with your fellow React Developers and give this article a clap. Hope you have a good day.

--

--

Sayan Sarkar

Senior Backend Engineer- System Design | Distributed System | Microservices | NodeJS