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:
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.
Having touched upon the necessity of Flux/ Redux in your React application, let’s dive right into the details of 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.
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:
For a visually representation of these concepts, let’s take a look at the diagram below representing the flow of data in Redux.
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:
Or it could be more modular like this:
As an example, the state for a Todo application would look like this:
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.
The signature for an action looks like this:
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:
We can send actions to the store using store.dispatch() as shown below:
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.
You guessed it right! Action creators are simply functions that create actions.
Take a look at the code below with the action creator addTodo.
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.
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:
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:
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.
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:
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:
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:
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.