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

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

Mohan Dere

Mohan Dere

Frontend developer with extensive experience in React, who loves teaching & writing.

Last updated on

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.  

Table of Contents

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 “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 places 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 sub tree A communicate with a component from sub tree 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 a direct component-to-component communication since it is error prone and can lead to spaghetti code. Instead React suggests to set 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 Publish–subscribe pattern inspired from Flux. It has been developed and maintained by Dan Abramov & 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 utilizes 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 visually 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: val1,
  state3: val1,
}

Or it could be more modular like this:

let store = {
  module1: {
    state1: val1,
    state2: val1,
    state3: val1,
  },
  module2: {
    state1: val1,
    state2: val1,
  }
  ...
}

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

let store = {
  todos: [   // state1
    {
      name: 'Buy a car'
      isCompleted: false
    }
  ],
  visiblityFilter: '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 store, we can initialise 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<<link to point 2>>.
  • Combining all reducers<<link to point 3>> 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. 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'
}

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 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 store. The output of action creators is then passed to the dispatch function. In the example above, addTodo will receive todo text entered by user as a 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 require 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

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.name,
        isCompleted: action.isCompleted,
      }
    ]);
  }
  return state;
};

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

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

We can then combine these 2 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 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 by default considered as synchronous that is Every time an action was dispatched, the state was updated immediately. 

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

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

Redux provides with 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:

 Sourcea hrefStackoverflow"/>
  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 here.

That’s it about Redux! So far we’ve understood 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.

// Find jobs by category

You've got the vision, we help you create the best squad. Pick from our highly skilled lineup of the best independent engineers in the world.

Copyright @2024 Flexiple Inc