5 min read

Introduction to Redux

Redux is an application state library for JavaScript, often used with React applications.
Introduction to Redux

Redux is an application state library for JavaScript, often used with React applications.

Web applications are large, complicated, and have many moving parts. Many things affect the state of these applications; what a user types in, whether or not they submit a form, what the user clicks, what order they perform operations in, and everything else under the sun. The user interface responds accordingly; as such, we can always fall back on the paradigm that the view is a function of state.

An identical application state should always result in an identical view, every single time the user enters that state.

Let's assume that we're using React, and have built a website with a shopping cart. There are many different areas on the website that can result in items being added to your cart; the product search page, the individual product page, related products to an individual product, and naturally the shopping cart itself.

We also have to have our shopping cart work in multiple places -- a page for the shopping cart, and a dropdown to preview the last item added to the cart and remove that item, or change the quantity of items purchased. That dropdown also lists the total cost of the items in the cart.

Without any form of state management and just relying on plain old component logic, we would have to do some intense callback passing and store our list of shopping cart items in a parent component's state that pass the list down to the dropdown page, or the dropdown.

Instead, libraries like Redux allow us to store our application state outside of our components in a store, which is a wrapper around the application state. This state, when in the store, can only be replaced through methods the store provides. In Redux, state is never manipulated -- it is simply replaced with a brand new state.

By storing the state in a central location, we will be able to have components hook into the same data no matter where they are in relation to other components. By forcing the state to be immutable and to be replaced instead of updated, we can very easily keep track of changes and see what actions cause what display to be generated. By preventing the state from being manipulated by anything outside of the store, we are forced to organize through the use of actions that replace the state.

Let's get to it

All talk and no code makes for a dull demonstration. Let's checkout a basic ecommerce website, where we have a product listing and the start of a dropdown shopping cart that lists the total price of the items, and the number of items in your cart.

When talking about Redux, a few terms are extremely fundamental:

  1. An action is a payload of information that describes how the store should be updated. All actions should be objects, with a type field that indicates what the action type is.
  2. An action creator is a function that will generate an action; you would pass parameters to the action creator, and it would generate an appropriate action
  3. A reducer is a function that consumes actions, and generates a new state. Reducers are supposed to be pure, and should not actually generate non-pure data. When given an identical action, a reducer should return an identical state.
  4. The dispatcher takes actions and passes them through our reducers; the dispatcher takes the new state that is returned from the reducers and provides it to the store, which then performs our store's update.

In Redux, our central state is composed from our reducers. In our case, we have two relatively independent pieces of data: our product listing, and our shopping cart, so we want a tree that's similar to this structure:

{
  products: [
    {
      id: 0,
      name: "Shampoo",
      description: "Shampoo: it's like soap, for your hair",
      cost: 5.99
    },
    {
      id: 1,
      name: "Neuhaus Chocolate",
      description: "For when you really need the best chocolate",
      cost: 15.75
    }
  ],
  cart: {
    0: 5,
    1: 1
  }
}

Our cart object would simply keep track of how many of each product we were purchasing, whereas our products array would actually keep track of what our products are.

In order to organize our store more easily, we can use the combineReducers method from redux to compose our product reducer and cart reducer into a single reducer that emulates the structure above, and have smaller reducers that only create a portion of the new state.

At this point, our reducers aren't doing anything -- we have to take our combined reducers and use them to create our state, but setting up our store.

If we go to our application's bootstrapper, index.js, we'll find the following lines:

import App from "./components/App";
import { Provider } from "react-redux";
import { createStore } from "redux";
import reducer from "./reducers";

const store = createStore(reducer);

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

We do a lot in those lines! We create a store from our reducers, which will generate our initial application state and setup all application state. But the store alone does absolutely nothing until we setup something to listen to it!

Natively, a redux store has a subscribe(listener) method, which runs the callback provided (listener) each time the store replaces the state. In order to fit into the React pattern of updating components when their properties are updated, we wrap our application in a Provider, which will trigger re-renders when values in the store update.

The final part of setting up our application is going to be creating our action creators. Our action creators are simple -- we simply make functions that return our action objects. Since we only have two, we're just going to keep them in one file for now:

let nextProductId = 4;

export const addProduct = (name, description, cost) => ({
  type: "ADD_PRODUCT",
  id: nextProductId++,
  name,
  description,
  cost
});

export const addProductToCart = (productId, count) => ({
  type: "ADD_PRODUCT_TO_CART",
  productId,
  count
});

In reality, we'd use a GUID generator to generate the next product id, rather than hardcoding some numbers in there.

Our component structure isn't actually anything out of the ordinary -- we have an App component that acts as the root of our application, which renders out the rest of our application.

Our ProductList component will be our simplest component to access the store in our examples. The react-redux package gives us a method, connect, that allows us to inject our state and actions as properties into our components.

class ProductList extends Component {
  render() {
    return (
      <div>
        {this.props.products.map(x => (
          <ProductRow
            addToCart={this.props.addToCart}
            product={x}
            key={x.id}
          />
        ))}
      </div>
    );
  }
}

const mapStateToProps = state => {
  return {
    products: state.products
  };
};

const mapDispatchToProps = dispatch => {
  return {
    addToCart: (id, quantity) => {
      dispatch(addProductToCart(id, quantity));
    }
  };
};

ProductList = connect(mapStateToProps, mapDispatchToProps)(ProductList);

Our mapStateToProps function takes a callback that injects properties based on the current state. In our case, we simply want to pull the list of products from the products portion of our state and simply assign it to a property of the same name, products. That allows our ProductList to access the list of products by simply accessing this.props.products.

Our mapDispatchToProps is a little more complicated -- the store's dispatcher is passed to our mapDispatchToProps, which will inject the property addToCart to our ProductList. When the addToCart method is run, the action to add a product to the cart is dispatched.

Because our ProductList pulls in all our state from Redux, our ProductRow is able to stay very lean and only contain relevant display data without touching the store.

When we add a product from the ProductRow, we fire off an action with a type of ADD_PRODUCT_TO_CART, which we can see consumed in our cartReducer.

The cartReducer will take the data from the action and generate our new state, which is consumed in our DropdownCart component. This component is very similar to our previous, but has a slightly more complicated mapStateToProps.

const mapStateToProps = state => {
  return {
    productDictionary: state.products.reduce((dict, product) => {
      dict[product.id] = product;
      return dict;
    }, {}),
    cart: state.cart
  };
};

Our properties don't always have to be exactly from the store, but can rather be generated based on the current state.

There's much more to Redux covered in the documentation. We'll cover asynchronous actions and how to integrate them into Redux in the coming articles!