Creating a Redux Store

Redux is a powerful library that provides a predictable state container to JavaScript applications written in any view library. Let's explore how it works by creating a generic store.

The use cases for Redux differ amongst teams. Redux is one of the different options that engineering teams may use to manage application state. As noted by its author, it may not always be needed; however, understanding how Redux works and the state management philosophy behind it is beneficial for any developer.

What You Need

  • Understanding of JavaScript.

Recipe Guide

The Purpose of Redux

It's ideal to abide by the philosophy of "Use the right tool for the job". To start, let's learn what are the jobs that are easier to handle by using Redux. From the Redux documents, we learn that it makes sense to use Redux when:

  • We have reasonable amounts of data that keep changing over time.
  • We need a single source of truth in our application to represent its current state at any time for any part of our application.
  • Keeping our state management logic within a top-level component is no longer sufficient to meet the needs or scale of our application.

These are suggestions on when to use Redux within a project more than actual guidelines. The scale, design, and size of a project ultimately end up defining if Redux is needed and when it's needed.

With that in mind, for this recipe, we are going to showcase the Redux philosophy and its power through a simple example. It's truly impossible to recreate a large-scale application for a blog post. Thus, our goal is to demonstrate how Redux works.

How Redux Works

Redux stores the complete state of our application in an object tree. This object tree is stored in a structure referred to as a store. A single store encapsulates the whole state of our application; it's the single source of truth.

An important principle of Redux is that we cannot change this object tree — or state tree — directly. Modifying the state tree by accessing its properties directly is what Redux solves. Why is that a problem? Changing or mutating the state directly may lead to data inconsistency that makes it impossible to clearly and easily predict the state of our application at any point in time. It can also be difficult to pinpoint where the state mutation happened to debug it.

Instead of mutating the state directly, Redux makes us emit actions that talk to the store and request a state change. These actions may deliver data that the store can use to update the state. An action is meant to describe clearly how the state should change.

The store needs a mechanism to listen to the actions being emitted and to define what logic to run for each action. In Redux, that's the role of a reducer. A reducer is nothing more than a switch block that receives an action and matches it with one of its cases. Upon a match, the case may have logic that modifies the state immutably to fulfill the request of that action or it may simply return the current state. A reducer is like a switchboard operator that directs each caller to the right place based on a provided name or phone extension.

A reducer's logic is wrapped in the body of a pure function. Functional purity is key to the concepts of Redux. A pure function is a function that given the same set of inputs returns the same output. One easy example of a pure function is a function that adds two numbers. If mathematically correct, you can always predict what the output of the function will be when two numbers are provided. No surprises.

To further understand this concept, let's look at the signature of a reducer function:

(state, action) => state;

Based on the concept of purity, we could say that given a specific state paired with a specific action, the reducer always returns the same state. The returned state is a new state because, within a reducer, we do not mutate the state directly. To create a history for the state, we clone the state object, modify it, and then replace the old state with this new state. Each action handled by the reducer creates a state record — which makes testing and debugging our application much easier!

A reducer describes how an action transforms the current state into the next state.

Creating a Reducer

Let's see what a reducer could look like for the state of a hunger meter:

function hunger(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

Notice something very important: the state parameter is initialized to 0. Why's that? Without an initial state, there is nothing we could transform. We always have to provide an initial state to the reducer — it's the first step in the state ladder.

Instead of passing action to switch as an argument, we passed it action.type. Let's learn more about that by exploring the anatomy of an action deeper.

Creating Actions

There’s not always a need for an action.type. We could have made the action in the hunger reducer be a string and filter through that and it would have worked. Why bother with a .type property? hunger is a simple reducer, its actions do not need to provide any external data to transform the state. However, what if we do need external data? We understand that we must always provide an action to the reducer, but if we need to provide it more information, such as a payload, what do we do? We create an action object:

const action = {
  type: "EAT",
  payload: "Burger"
};

Making the shape of this action our standard structure will make it easy to communicate both actions and external data to our reducer. If we need to send more than just a string as the payload, we do not need to create a third property for action, we just simply make payload an object and store each piece of information as one of its properties.

Keeping the structure of our actions concise and predictable will allow us to scale Redux much better within our team and across our codebases. Limiting our action to having just a type and a payload is part of the recommended Flux Standard Action pattern.

Redux Action Constants and Action Creators

Imagine having to type the structure of an action all around your code. It is prone to error. A small typo on an action type will send it straight to the default case. We want to have a solid way to create actions that decrease the chances of error. To achieve this goal, we can use action constants and action creators.

Action Constants

Let's dig into action constants first. An action constant is simply a way to type the string that represents our action once and then we can forget about it. The string could be assigned to a constant variable that we would then refer whenever we need the action. Since we are assigning a literal to a constant, the constant's value cannot be mutated after assignment — completely protecting us against our actions having typos or representing anything different than what was originally intended.

An action constant looks like this:

const EAT = `EAT`;

It may look redundant to assign EAT to EAT but once you type that action string and assign it to the constant, you do not longer have to type its literal value again. On top of that, you can benefit from using your IDE or Code Editor suggestions to quickly refer to the action constant.

Let's recreate our example action, EAT using an action constant:

const EAT = `EAT`;

const action = {
  type: EAT,
  payload: "Burger"
};

That looks much better and easier to manage. We still have something cumbersome and repetitive to do: whenever we want to send the EAT action to a reducer, we would need to create the action object. Wouldn't it be neat if we had an easier way to create an action and save some typing? Enter Action Creator!

Action Creators

An action creator is simply a helper function that creates actions for us. The goal of the action creator is to supersede the action itself. For that reason, the name of the action creator should match the name of the action and it should also encapsulate its action constant:

function eat(meal) {
  return {
    type: EAT,
    payload: meal
  };
}

Look at that! By using an action creator, we only need to use our action constant once. Whenever we need to use the EAT action, we simply call the function eat and provide it an argument that would serve as the action's payload.

Using functions to create actions will make it very easy to test our code.

Now that we understand how to create actions, let's see how we send those actions to the store — or how we dispatch them. But before we can do any of that, we need an actual store. Let's learn how to create a Redux store next.

Creating a Redux Store

To create a store, we need to import the createStore function from the redux module and pass it a reducer.

Going back to the hunger reducer example, this is what we can do:

import { createStore } from "redux";

const INCREMENT = `INCREMENT`;
const DECREMENT = `DECREMENT`;

function increment() {
  return { type: INCREMENT };
}

function decrement() {
  return { type: DECREMENT };
}

function hunger(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

const store = createStore(hunger);

Our actions do not need a payload so we do not specify it; but, we do keep the action object structure. By passing the hunger reducer to the store, we are basically registering that reducer with it and allowing it to handle the actions defined by the hunger reducer.

Now that we have a store in place. We can interact with it through an action dispatch.

Dispatching Actions to the Redux Store

As we previously discussed, the only way that Redux allows us to transform the state of the application is through actions that define how the transformation should take place. To communicate our request to the store, we dispatch an action to it:

import { createStore } from "redux";

const INCREMENT = `INCREMENT`;
const DECREMENT = `DECREMENT`;

function increment() {
  return { type: INCREMENT };
}

function decrement() {
  return { type: DECREMENT };
}

function hunger(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

const store = createStore(hunger);

store.dispatch(increment());
store.dispatch(increment());
store.dispatch(decrement());

The first dispatch would make our initial state change from 0 to 1. The second one makes it 2 and the third one makes it 1 again. Our action creators make dispatching very easy and clean!

This small example is almost ready. The last thing that we need to learn is how to get the state for our application to use it within the logic of our code.

Getting the State from the Redux Store

There is no science on this step. When we create a store, we get the following API: subscribe, dispatch and getState. We already used dispatch to send actions to the store. Let's discuss what the other two do.

To connect to the store, we need to subscribe to it. subscribe takes a callback that acts as a listener and it's called whenever the store state changes after an action dispatch. That's it. The body of the subscribe listener is the safest place to access the state of the store because it would be aware of any changes that have been made to it.

To get the state of the store, we use the method getState. Notice how if getState is called outside of store subscription, we may miss updates to the store since it may not be called again once a state change happens. Within the subscription, we may store the value of the state into a local variable or in localStorage.

We are done taking a deep dive into how Redux works. It's simple, straightforward and, above all, predictable. Within the context of React, we'd use bindings to connect to the Redux store without having to subscribe to it directly; and, that is what we are going to explore in another recipe!

Enjoy