Making sense of Redux

22 June, 2020

These days, there is an increasing use of single page applications (SPA), progressive web apps (PWA), Electron-based desktop apps and React-Native mobile apps. But why?

Variety of trending technologies

The reasons are simple and justifiable.

  • All these apps are written in JavaScript and allow quick iterations.
  • The end-users don't even experience any difference.
  • The tooling and community is developer-friendly, so the app developers are usually happy.
  • Management have to worry less about hiring the experienced developers.
  • And, app owners can focus on increasing their business instead of worrying about technical glitches in their app.

But that doesn't mean everything is good in terms of developer experience and the challenges faced while building such apps.

Complex app state

There are very popular frontend frameworks such as React.js, Vue.js, Angular.js etc. that allow building SPAs, PWAs and desktop apps (using Electron) very easily. React-Native helps building mobile apps with the same ease. Each of these frameworks or technologies offer features to build and render the app views with less hassle, support dynamic routing, and also, support managing the app's state at some extent.

Since the old-school way of reloading the whole page to refresh the app's state doesn't make any sense now, the app has to store and manage the state locally and keep it in sync with the backend. Some developers try to keep the state centralized, while some keep it distributed within the app components.

Complex user interfaces may depend on the data that sometimes is common. So by fetching the same data over again to render some other parts of the app can often lead to bad user experience and unnecessary processing on the backend. But sharing such a common data across the app can be very challenging since it needs to be constantly cached, invalidated and updated.
Complex app state

The state can consist of many things and not just these —

  • Responses from backend for various resources.
  • Cached data.
  • Local data that's not synced with the backend yet.
  • UI state information such as active tabs, AJAX loading state, pagination criteria, etc.

There are so many options available to manage such a complex app state conveniently. Among which, Redux is the most popular, community-driven state management library.

What's Redux?

Redux is a standalone, tiny JavaScript library. That means it can be used in any JavaScript project and it also works very nicely with the popular frameworks such as React.js, Vue.js, Angular.js, etc.

Redux inherits a lot of ideas from the functional programming paradigm. It recommends writing pure, side-effects free and immutable code. But JavaScript isn't a pure functional programming language and doesn't restrict us from writing mutable code. That's why Redux abstracts a lot of things for interacting with the state and hides them from the developers to provide a highly predictable behaviour.

Because of the predictable (deterministic) behaviour, it is trivial for Redux to support features like undo-redo and time-travel debugging.

High level illustration of Redux flow

Redux is essentially build upon just three main concepts.

1. Centralized store

The whole application state is stored in a single plain JavaScript object. This object is called as Redux store. The object can have nested information. It is recommended to put only those values in the Redux store which are JSON-serializable. Because the state is centralized, it can be easily fed with an initial data while server-rendering the app.

2. State is immutable

We can access the state in read-only mode. That means, we cannot mutate the state directly. To change the state, Redux require us to dispatch actions. Actions are just plain JavaScript objects that describe the intent. Redux applies all such actions on the centralized store one-by-one to avoid any race conditions.

// An example "action" object.
// Only `type` key is mandatory, rest of the payload is optional.
{
  type: "PUBLISH_ARTICLE",
  id: "my-awesome-article-1234",
  tweet: true
}

3. Reduce the state

These applied actions are passed to a plain JavaScript function that is known as a reducer function. The reducer function transforms the previous state to a new one according to the passed action as intended. The reducer function shouldn't mutate the previous state directly, instead it should copy the previous state and modify that copy. The reducer function should be a pure function. There is just a single reducer function but it's very easy to split it into smaller composable reducer functions as per our needs.

// Signature of a "reducer" function.
(oldState, action) => nextState

So we now know a bit about Redux store, actions and the reducer. But how does these things work together?

Using Redux

Redux in action!

We can create the Redux store but we need to specify a reducer to do that. So let's create a reducer first.

Implementing a reducer

Reducer is a simple function that takes the old state and an action as arguments and returns a new state. It implements how an action (having mandatory field type) transforms the old state into a new one. If the reducer cannot recognize the given action, it must return the previous state as is.

// This is the shape of our example app state.
// Structuring the app structure is totally up to us.
// It just needs to be a plain JavaScript object with JSON-serializable values.
const initialState = {
  articles: [],
  comments: [],
  users: [],
};

function myAppReducer(state = initialState, action) {
  switch (action.type) {    case "ADD_ARTICLE":
      // Notice that we are NOT mutating the 'state' object here. Instead we
      // are using ES6 spread operator to create a new object from it by
      // concatenating the specified article to an existing list of articles.
      return { ...state, articles: [...state.articles, action.article] };

    case "UPDATE_ARTICLE":
    // Returns new state with the updated article...

    case "PUBLISH_ARTICLE":
    // Returns new state by marking the specified article as published...

    case "UNPUBLISH_ARTICLE":
    // Returns new state by unpublishing the specified article...

    case "ADD_COMMENT":
    // Returns new state by adding a new comment...

    // More cases here...

    default:
      return state;
  }
}

Look at the number of cases in that switch-case expression. That list can just keep increasing. Can we make it better?

Of course, we can!

// This reducer receives the previous state with just the list of articles.
function articlesReducer(state = [], action) {  switch (action.type) {
    case "ADD_ARTICLE":
      return [...state, action.article];

    case "UPDATE_ARTICLE":
    // Returns new state with the updated article...

    case "PUBLISH_ARTICLE":
    // Returns new state by marking the specified article as published...

    case "UNPUBLISH_ARTICLE":
    // Returns new state by unpublishing the specified article...

    // More cases here...

    default:
      return state;
  }
}

// And this reducer receives state of comments.
function commentsReducer(state = [], action) {  switch (action.type) {
    case "ADD_COMMENT":
      return [...state, action.comment];

    // More cases here...

    default:
      return state;
  }
}

// And this reducer gets the user list as the state.
function usersReducer(state = [], action) {  // ...
}

function myAppReducer(state = {}, action) {
  return {
    articles: articlesReducer(state.articles, action),    comments: commentsReducer(state.comments, action),    users: usersReducer(state.users, action),  };
}

We split the myAppReducer into smaller reducers that deal with transforming only certain parts of the overall state. We can extract those split reducers into their own files if we want to.

Since, splitting and combining reducers is a very common practice, there's combineReducers higher order function that simplifies the process of combining the multiple reducers into a single reducer.

import { combineReducers } from "redux";

const myAppReducer = combineReducers({
  articles: articlesReducer,
  comments: commentsReducer,
  users: usersReducer,
});

Because we now have a reducer function, let's go and create the store.

Creating store

Creating the Redux store is straightforward.

import { createStore } from "redux";

// Pass the `myAppReducer` reducer function we just implemented.
const store = createStore(myAppReducer);

This store object holds the whole app's state.

Creating and dispatching actions

An action is a plain JavaScript object that mandatorily has a type property that indicates "what it does". There could be or could not be additional properties in this object apart from the type property.

Here are some example actions.

// An action without any payload
{ type: "TOGGLE_DARK_MODE" }

// An action with payload
{
  type: "LOGIN",
  email: "dan@example.com",
  password: "dan'sSuperDuperSIMPLEPassword",
}

To change the Redux state, we need to dispatch an action.

The Redux store object that we defined above has the dispatch() method. We must use it to dispatch an action.

// Say, we want to add an article to the store.
// So, we dispatch an action of type "ADD_ARTICLE".
store.dispatch({
  type: "ADD_ARTICLE",
  article: {
    id: "my-awesome-article-1234",
    title: "My awesome article",
    description: "I will write it tomorrow",
    published: false,
  },
});

This can be okay in smaller apps. But in large codebases, writing down that whole action structure by hand can be tricky and error-prone, and would require changing all the occurences in the codebase when its structure changes.

To simplify, we abstract this logic into an action creator.

An action creator is a function that creates an action. That's it.

// This action creator returns an action of type "ADD_ARTICLE".const addArticle = (title, description) => {  return {    type: "ADD_ARTICLE",    article: { id: generateId(title), title, description, published: false },  };};
const updateArticle = (id, title, description) => {
  // Returns an action of type "UPDATE_ARTICLE"...
};

// More action creators here...

And this is how we can dispatch an action now by using an action creator.

store.dispatch(addArticle("My awesome article", "I will write it tomorrow"));

When an action is dispatched, Redux calls the reducer function. In our case, Redux calls the myAppReducer function. Since myAppReducer is combined of multiple reducers, all those reducer functions (such as articlesReducer, commentsReducer and usersReducer) are called one by one in the defined order with that action. The articlesReducer handles this action and updates the store accordingly. Other reducers can also handle this action as well if needed.

Async action creators

So far, we have been discussing the actions which are synchronous in nature. The state gets updated immediately when a synchronous action is dispatched. But the world is cruel. Not all actions happen instantly. They happen after some delay and often involve performing some side effects. Some common examples of asynchronous actions are — making an HTTP/AJAX request, generating a random number, manipulating DOM, interacting with LocalStorage, etc.

Consider this situation.

We want to fetch the articles from our backend API. To do so, we simply CANNOT update the store immediately with the latest articles from the backend by just dispatching a single FETCH_ARTICLES action. We must make an HTTP call to our backend to fetch the articles and then based on the asynchronous outcome, update the store accordingly. This whole operation would consist of following actions.

  1. Action FETCH_ARTICLES_INIT to indicate that the asynchronous HTTP request began. Reducer may handle this and toggle a flag such as fetchingArticles in the state to true. UI can show a loading spinner by reading this flag from the state.
  2. Action FETCH_ARTICLES_SUCCESS to indicate that it contains latest articles from the backend. Reducer updates the articles in the state accordingly. Reducer can also toggle the flag fetchingArticles to false. UI would be refreshed with the latest articles.
  3. Action FETCH_ARTICLES_FAILURE to indicate that the HTTP request failed for some reason. Reducer may handle this action and toggle the flag fetchingArticles to false as well as store the error message in the state so that UI can display that error to the user.

We can dispatch these various actions ourselves but that can be tedious. To make it easy, there are some Redux middlewares available. Redux Thunk, Redux Saga, Redux Observable are some popular ones among them. Redux Thunk is a standard way to deal with the async actions. Other middlewares can be used to tackle more advanced scenarios.

When Redux Thunk middleware is used, an action creator can return a function instead of an action object. Such an action creator is known as an async action creator and it becomes a thunk. The Redux Thunk middleware executes such a function returned by an action creator. That returned function can perform side-effects and dispatch actions as we wish.

This is how our async action creator would look like.

import fetch from "cross-fetch";

// This is a synchronous action creator.
const fetchArticlesInit = () => ({ type: "FETCH_ARTICLES_INIT" });

// This is also a synchronous action creator.
const fetchArticlesSuccess = ({ articles }) => ({
  type: "FETCH_ARTICLES_SUCCESS",
  articles,
});

// This is a synchronous action creator as well.
const fetchArticlesFailure = error => ({
  type: "FETCH_ARTICLES_FAILURE",
  error,
});

// This is an async action creator.const fetchArticles = () => {  // The returned function can dispatch actions with the received  // `dispatch` handler (1st argument) and can retrieve the current  // state using `getState` handler (2nd argument).  return (dispatch, getState) => {    dispatch(fetchArticlesInit());    fetch(`/api/v1/articles`)      .then(response => response.json())      .then(json => dispatch(fetchArticlesSuccess(json)))      .catch(e => dispatch(fetchArticlesFailure(e.message)));  };};

The logic to create the Redux store needs to be updated a bit when we use a middleware like Redux Thunk.

import thunkMiddleware from "redux-thunk";import { createLogger } from "redux-logger";
import { createStore, applyMiddleware } from "redux";

const loggerMiddleware = createLogger();

const store = createStore(
  myAppReducer,
  applyMiddleware(thunkMiddleware, loggerMiddleware));

// We dispatch our async action creator similar to how we dispatch// the synchronous ones. There's no difference on the dispatch side!store.dispatch(fetchArticles());

Retrieving state

We can manually fetch the current state using getState() method on the store object.

store.getState();// 👆 This would return the current state
// that would be a plain JavaScript object:
//
//      {
//        articles: [
//          {
//            id: "my-awesome-article-1234",
//            title: "My awesome article",
//            description: "I will write it tomorrow",
//            published: false,
//          },
//        ],
//        comments: [],
//        users: [],
//      }

But how to automatically get notified whenever the store gets updated due to some action?

For that, we need to subscribe() to the store.

Subscribing to the store updates to stay in sync

Whenever store is updated after the reducer returns a new state because of a dispatched action, Redux invokes all the listeners previously registered using store.subscribe(listenerFunction) method.

// This listener function gets called only when the store is updated.
const updateUI = () => {
  const updatedState = store.getState();

  // Logic to update the UI with the `updatedState` here...
};

store.subscribe(updateUI); // returns a function to unsubscribe this listener

Note that the listener function doesn't receive any information. So if we want, we have to call store.getState() method to fetch the updated state within that listener.

Redux with React

Keeping UI in sync with the state by ourselves can be challenging. To make our life easier, Redux officially provides binding for React.

With the help of official react-redux package, using Redux in a React app then becomes very easy.

The main React component needs to be enclosed within the <Provider /> React Redux component that makes the Redux store available to the whole React app.

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";

import store from "./store";
import App from "./App";

ReactDOM.render(
  <Provider store={store}>    <App />  </Provider>,  document.getElementById("root")
);

As we can see, thanks to React Redux — we don't have to worry about manually subscribing/unsubscribing to the store!

Now onwards, to connect any React component to the Redux store, we use connect() helper function.

import { connect } from "react-redux";
import { fetchArticles, toggleDarkMode } from "./actionCreators";

const ArticleListing = ({ articles, fetchArticles }) => (  <div>
    <ul>
      {articles.map(({ id, title }) => (
        <li key={id}>{title}</li>
      ))}
    </ul>
    <button onClick={fetchArticles}>Refresh</button>
  </div>
);

const mapStateToProps = (state /*, ownProps*/) => ({  articles: state.articles,
});

const mapDispatchToProps = (/* dispatch */) => ({  fetchArticles,
  /* toggleDarkMode, */
});

export default connect(mapStateToProps, mapDispatchToProps)(ArticleListing);

The connect() function accepts two optional arguments.

1. mapStateToProps

We provide this function to only select the data from the store that the connected component needs. It is called every time the store is updated. In our example above, we select the articles property from the state object (line #16). The articles property then is accessible as a prop by the connected component (line #4). This way the connected component can re-render itself whenever the state changes and keep itself in-sync with the state.

2. mapDispatchToProps

If this argument is NOT specified, the connected component receives the Redux store's dispatch function as a prop. We specify this argument as a function to receive the needed action creator functions as props in the connected component so that we can call props.fetchArticles() directly instead of calling props.dispatch(fetchArticles()) to dispatch an action.

Finally, one last question that many of us may have to answer often.

When to use Redux and what goes inside it?

Many applications may NOT need Redux at all. But if we discover that we are writing so much repetitive and low-level code to manage the app's state by-hand — and we are finding it hard to extend, manage and debug that custom state management logic then Redux is definitely a recommended option to roll in.

But Redux doesn't care or ditctate about what data we store in it and how we store it. We need to make that call. This is a good thing but that often makes it confusing for us to decide what to store and what not to store in Redux. Storing things like form state could hit Redux store on every key stroke.Imagine that — hitting the Redux store on every keypress! That could hamper the app's performance a lot. Therefore, we should NOT be putting everything in the Redux store. Instead, we must find out the data that other parts of application care about or the data that drives multiple components. Such a shared data can be stored inside Redux. But that's just an opinion. Everyone's situation is different. So we must study our application carefully and ensure that we are not over-using Redux or making its usage unnecessarily complex.

Final thoughts

We didn't cover many other important and advanced topics such as server rendering, caching, state selectors, normalizing data, and many more. But we know that they can be explored as per our needs.

Hopefully, Redux should be making a little bit of sense to you.

That's it for now, folks. Until then, happy Reduxing!

Enjoyed reading this article? Then you may also like my handwritten book on
Networking Microservices using Consul.

Networking Microservices using Consul

To receive updates of my new articles, either subscribe via RSS feed or join my newsletter.

I hate spams as much as you do! I won't spam you with unwanted emails.