Flexiple Logo
  1. Home
  2. Blogs
  3. React
  4. Learn Redux and its usage with React by building Books finder app

Learn Redux and its usage with React by building Books finder app

Author image

Mohan Dere

Software Developer

Published on Wed Mar 16 2022

This tutorial is broadly divided into 2 parts – In the first part (this post), we’ll learn Redux and its core concepts while in the second, we’ll learn how to manage state across an entire React application efficiently by building an actual application (Books finder app) using Redux.

TLDR: You can checkout source code of bookfinder-app on Github and can play around if you are already familiar with Redux and React router.

Understanding the Component Tree and data flow in React

Component is the core of modern frameworks like React/Vue. Components let you split the UI into independent, reusable pieces. You should think about each piece in isolation as a Single Responsibility Unit which should ideally only do one thing. When building an app, we combine components together to form a tree called Component Tree.

If you have been working with React, you would know that React follows a “top-down” or “unidirectional” data flow approach. To break this down, passing data from parent components down to child components is termed as “flow” and when this takes place only one way, we call it “unidirectional.”

Component Tree with unidirectional data flow raises the common challenge of being able to share data among different nodes at different levels. Here are a few questions you might need to tackle:

  • How do you communicate between two components that don’t have a parent-child relationship?
  • How do you pass data deep down in the tree?
  • How does a component from subtree A communicate with a component from subtree D?

The diagram below depicts React’s unidirectional data flow:

React unidirectional data flow
React unidirectional data flow

Tackling the challenges of unidirectional data flow with Flux/Redux

In order to handle these challenges, React doesn’t recommend direct component-to-component communication since it is error-prone and can lead to spaghetti code. Instead, React suggests setting up your own global event system like Flux/Redux.

Redux offers us a central place called “store” that helps us save our application state. To update the store, the source component has to emit an action, which is essentially an object describing the event along with the new state. Once the store is updated, the receiver/subscriber component(s) get the updated state as shown in the diagram below.

Redux store
Redux store

Having touched upon the necessity of Flux/Redux in your React application, let’s dive right into the details of Redux.

Learn Redux

Redux is a global state management (both data-state and UI-state) library for SPAs. It is framework agnostic, i.e., it can be used with any framework of your choice or with vanilla JS. Redux is essentially an implementation of the Publish–subscribe pattern inspired by Flux. It has been developed and maintained by Dan Abramov and a large active community.

Prerequisites

Before moving to understand Redux and how to use it, I would recommend you to read up about the Publish–subscribe pattern followed by the fundamentals of functional programming (FP). Although this is not mandatory, it will put you in a better place to understand Redux.

React, Redux, and in fact, modern JS utilize a lot of FP techniques alongside imperative code such as purity, side-effects, function composition, higher-order functions, currying, lazy evaluation, etc. Before moving further, I suggest you take a look at the following concepts from FP:

Basic concepts in Redux

In this section, we will go through the basic concepts of Redux listed below:

  1. Store
  2. Action
  3. Reducer
  4. Middleware

For a visual representation of these concepts, let’s take a look at the diagram below representing the flow of data in Redux.

Redux data flow
Redux data flow

Store

Store represents the state of our application. The entire application state is stored in an object tree.

It can be a plain object as shown below:

let store = {

  state1: val1,

  state2: val2,

  state3: val3,
}

Or it could be more modular like this:

let store = {

  module1: {

    state1: val1,

    state2: val2,

    state3: val3,
  },

  module2: {

    state1: val1,

    state2: val2,
  }
  ...
}

As an example, the state for a Todo application would look like this:

let store = {
  todos: [   // state1
    {
      name: 'Buy a car',
      isCompleted: false
    }
  ],
  visibilityFilter: 'completed', // state2
  ...
}

In real-life applications, we would likely have multiple modules. So it’s a good practice to maintain global state in the same way.

Important points to note:

  • State is nothing but a plain JS object with a tree.
  • By default, on page reload, state cannot be persisted – you have to explicitly do it if you need to.
  • While creating a store, we can initialize it with a default state or it can be hydrated with server data.
  • The only way to change the state is to emit an action.
  • Combining all reducers forms the state tree.

Note: Do not use store and state interchangeably since these are different terms. Store holds/represents state(s) and state can be further divided into sub-states.

Action

Actions are the only way of sending data from your application to the store. An action is just a plain JavaScript object with a mandatory type property. Actions can be dispatched from a view/component in response to user actions.

The signature for an action looks like this:

{
  type: 'MY_ACTION'
}

An action can be any object value that has the type key. We can send data along with our action (conventionally, we’ll pass extra data along as the payload of an action) from our application to our Redux store.

We create and dispatch actions in response to user or programmatically generated events. For example button click, network request started/completed.

Here’s an example action which represents adding a new todo item:

{
  type: 'ADD_TODO',
  data: {
    name: 'buy car',
    isCompleted: false
  }
}

We can send actions to the store using store.dispatch() as shown below:

dispatch({
  type: 'ADD_TODO',
  data: {
    name: 'buy car',
    isCompleted: false
  }
})

Note: Don’t worry about the dispatch function as of now. We will look at it in later sections.

Important points to note:

  • Actions are plain JS objects and must have a type property which indicates the type of action being performed.
  • Actions can have data that is put in the store.
  • Actions can be synchronous or asynchronous.

Action Creators

You guessed it right! Action creators are simply functions that create actions.

Take a look at the code below with the action creator addTodo.

function addTodo(data) {
  return {
    type: 'ADD_TODO',
    data
  }
}
// Later
dispatch(addTodo({
  name: 'buy car',
  isCompleted: false
}))

Action creators are called from components along with data/payload sent to the store. The output of action creators is then passed to the dispatch function. In the example above, addTodo will receive todo text entered by the user as data.

Async Actions

In the previous example, adding a todo action results in synchronous code execution. By default, actions in Redux are dispatched synchronously, which is a problem for any non-trivial app that needs to communicate with an external API or perform side effects.

While performing an asynchronous operation, there are two crucial timestamps:

  • The moment you start the operation (start of an API call), and
  • The moment you receive an answer (when the API call succeeds or fails).

Each of these two moments usually requires a change in the application state. In order to do that, you need to dispatch normal actions that will be processed by reducers synchronously.

For example, for any API request, you’ll want to dispatch at least three different kinds of actions:

{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }

Reducer

A reducer is a pure function responsible for receiving the current state, dispatching action as an argument and returning a new state.

Take a look at the example below:

const todos = (state = [], action) => {
  if (action.type === 'ADD_TODO') {
    return Object.assign({}, state, [
      ...state,
      {
        name: action.data.name,
        isCompleted: action.data.isCompleted,
      }
    ]);
  }
  return state;
};

const visibilityFilter = (state = 'all', action) => {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter;
    default:
      return state;
  }
};

We have used two reducers todos and visibilityFilter. For todos, the default state will be an empty array of todo items.

We can then combine these two reducers using the combineReducers function.

import { combineReducers } from 'redux'

export default combineReducers({
  todos,
  visibilityFilter
})
Reducers
Reducers

Important points to note:

  • Multiple reducers can be combined together to create the final application state.
  • Ideally, we can combine reducers one module at a time and finally create a root reducer.
  • Reducer function name is simply the state (key) from the store. For the example above, our store would look like this:
{
  todos: [{ // state1
    name: 'Buy a car',
    isCompleted: false
  }],
  visibilityFilter: 'all'
}

Middleware

In Nodejs(Express js), Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle.

Middleware functions can perform the following tasks:

  • Execute any code.
  • Make changes to request and response objects.
  • End the request-response cycle.
  • Call the next middleware in the stack.

Let’s look at the example below:

var express = require('express')
var app = express()
...
// Middleware
var logger = function(req, res, next) {
  console.log(`Request url is: ${req.headers.host}${req.url}`);
  next();
}
app.use(logger);
...
app.listen(3000)

This is a simple express.js application. We’ve just defined the middleware function called “logger” which will log the request url every time the app receives a request. To load/use the middleware function, we call app.use(). To pass on the request to the next middleware function, we call the next() function.

For Redux, middleware is used to solve a different set of problems than express.js, however, both are conceptually the same. For Redux, it provides a third-party extension point between dispatching an action, the moment it reaches the reducer.

Why do we need middleware in Redux?

In Redux, all operations are by default considered synchronous, that is, every time an action is dispatched, the state is updated immediately.

But how do async operations like a network request work with Redux? As discussed earlier, reducers are the place where all the execution logic is written, and the reducer has nothing to do with who performs it.

In this case, the Redux middleware function provides a way to interact with dispatched actions before they reach the reducer. Custom middleware functions can be created by writing higher-order functions (a function that returns another function), which wraps around some logic.

Redux provides an API called applyMiddleware which allows us to use custom middleware as well as Redux middlewares like redux-thunk and redux-promise.

Important points to note:

  • Without middleware, Redux only supports synchronous data flow.
  • Middleware sits between the action and reducer and is responsible for side effects.
  • It can dispatch multiple actions at different points in time, e.g., when a network request begins and completes.
  • Middleware can be synchronous or asynchronous like Redux thunk.

Here is how async middleware redux flow can be imagined:

 Source: Stackoverflow
Source: Stackoverflow

On a closing note, it is important to note that Redux is designed around three fundamental principles. It is necessary that you understand them before delving into building your application with Redux. You can read about them here.

That’s it about Redux! So far we’ve understood the basics of Redux. It’s now time to use Redux to our advantage in a real-life application. In the next article, we will delve into building a “Books finder” app applying all that we have learned about Redux.

Related Blogs

Browse Flexiple's talent pool

Explore our network of top tech talent. Find the perfect match for your dream team.