Redux explained

February 17, 2020

Redux explained

Redux - created by Dan Abramov is by far the most popular state manager among React developers. Since managing state is an issue many of us will run into sooner or later, chances are that you came across this library. If you are new to Redux, it may feel a bit intimidating at first. Actions, reducers, the store… What are those? I know, there’s terminology all over the place. In this blog post, I’ll explain the basics of Redux and clarify whether or not you should use it. It’s about a 10 minute read.

Should you use Redux

In my previous blog post I wrote about React Hooks and how to manage global state using the context API together with useReducer and useContext. Since Redux is a JavaScript state manager, you may wonder “do we really need Redux to handle this?”. Well, if all you need from Redux is the ability to pass data around, the answer is no. However, as your React application is growing and getting more complex, keeping track of multiple data flows and state updates can turn into a real struggle. At some point you may want to reach for a consistent way to manage all these simultaneous events - that’s when Redux comes in.

With Redux the state of your app is centralized in one location; the store. Redux uses pure functions to update the state, which means it returns the same value if the same input occurs again. No side effects. This makes the state predictable. In addition, Redux comes along with a DevTools extension, which highly improves the ability to debug our application’s state updates. But wait. There’s even more. Redux’ recent release version 7.1 includes a long awaited support for React Hooks. This enables us to use React Hooks and Redux side by side in functional components. Besides that, Redux has provided us with its own Hooks; useSelector and useDispatch. More on that later.

The store

As mentioned earlier, Redux centralizes the state of your application in one location; that’s what we call the store. It’s based on Redux’ first principle:

“A single source of truth”

The store is where it all comes together. The epicentre of our application. It holds our global state. Let’s say we want to create a shopping cart. To keep things simple, our state consists of two elements: a list of items available in our shop and a list of items that have been added to the shopping cart by the user. This is how I created a store using createStore from the Redux library:

import { createStore } from 'redux';
import rootReducer from './reducers';
import { composeWithDevTools } from 'redux-devtools-extension';

const initialState = {
  items: [
    { id: 1, title: 'SOAP', price: 3 },
    { id: 2, title: 'MILK', price: 0.99 },
    { id: 3, title: 'EGGS', price: 1.99 },
  ],
  addedItems: [],
}

export const initializeStore = (state = initialState) => {
  return createStore(
    rootReducer,
    state,
    composeWithDevTools(),
    // other store enhancers if needed 	
  )
};

Here, I initialized the Redux store by passing the initial state, a rootReducer and the composeWithDevTools extension (enables us to use the DevTools extension in our browser). The initial state provides us with the available items in our shop, which we’ll display later and a list of addedItems (empty for now). What about the rootReducer? This will be clarified later, but it’s just a file where all reducers in our application get combined.

Mind you, there’s only one single store in a Redux application. At the same time, the store has only one source of information; actions.

Actions

Actions are nothing more than plain JavaScript objects. They provide the store with data from our application. This leads to Redux’ second principle:

“State is read-only”

In other words, the state is immutable. There’s only one thing that triggers a state change: “dispatching an action” aka sending out a signal to the store. So, when does this happen? In our case, it happens only in one situation; when a user adds an item to the cart. In a real application you’ll need to deal with multiple actions being dispatched, caused by different user inputs. For this reason, an action must have a type property that indicates which action has been dispatched. Optionally, we can provide the action with data, what we call the payload. This is how it looks like:

export const ADD_TO_CART = 'ADD_TO_CART'

export const addToCart = id => {
  return {
      type: ADD_TO_CART,
      id
    }
};

Here, I defined a simple function. This is what we call the action creator. It accepts an argument and returns an object; the action. The action is provided with a payload; in this case id, but it can be whatever you like or might be omitted if not needed. Later on, this payload will be used by the reducer to update the state. The action type is exported as a string constant to avoid misspelling.

Now, let’s take a look at how an action gets dispatched. That’s where Redux’ Hooks useSelector and useDispatch come in:

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { ADD_TO_CART } from '../../redux/actions/cartActions';

const Shop = () => {

  const items = useSelector(state => state.items);
  const dispatch = useDispatch();

  const handleClick = id => {
    dispatch({type: ADD_TO_CART, id})
  }

  return (
    <ul>
      {items.map(item => (
       <li key={item.id}>
        <button onClick={()=>{handleClick(item.id)}}>Add</button>
       </li>)
    )}
  </ul>)
}

export default Shop;

To display the list of items we have available in our shop, we need some way to extract this data from our store. Thankfully, useSelector will help us with that. This Hook will accept the entire Redux store as its only argument and we only need to specify what we need from the store; state.items. After our items are displayed, a user adds an item to the shopping cart. Now, we need to send out a signal to our store. As mentioned earlier, the only way to communicate with our store is by dispatching an action. After being imported from the Redux library, useDispatch will handle this for us. It fires off an action with the item’s id as its payload. Notice, that I first saved this Hook instance as a variable before using it in the event handler.

Remember that actions only describe what happened, not how the state should update. We leave this major decision up to the reducers.

Reducers

A reducer is a pure function and accepts two arguments; the previous state and an action. It will generate the next state based on this and according to Redux’ third principle:

“Changes are made with pure functions”

This means a reducer must always return the same value, when given the same inputs, without causing any side effects. Causing “side effects” basically means modifying things outside the function’s scope. For example, making an API call, modifying global variables or even using a console.log() statement. At the same time, the state is immutable. For this reason, reducers must always return a new version of the state instead of mutating the existing one. This allows us to do things like time travel debugging, as the DevTools extension provides us with a record of every action dispatched by the user. With that in mind, let’s create a reducer that will update the addedItems list. A reducer can be written as a simple switch statement; depending on what action type comes in, it decides how the state should update:

import { ADD_TO_CART } from '../actions/cartActions';

const cartReducer = (state, action) => {
    switch (action.type) {
        case ADD_TO_CART:
            let addedItems = state.items.find(item => item.id === action.id)
            return {
                ...state,
                addedItems: [...state.addedItems, addedItems]
            }
        default:
            return state
    }
}

export default cartReducer;

This reducer verifies what has happened, based on action type. You may notice the action type ADD_TO_CART we exported earlier, is imported here. Also, the item’s id we provided as payload, is used by the reducer to find the corresponding item and add this to the addedItems array. Notice how the spread operator is used to avoid state mutations. Instead of mutating the state directly, it generates a copy first. That’s all; an item has been added to the shopping cart without mutations or side effects.

In this case there’s only one reducer, but as your application is growing you’ll need multiple reducers to separate unrelated parts of your state logic. In order to connect them we use combineReducers from the Redux library:

import { combineReducers } from 'redux'
import cartReducer from './cartReducer'

export default rootReducer({
    shop: cartReducer,
    // other reducers	
})

You might remember, we imported this rootReducer at the beginning when we initialized the store. With this we put all the pieces together and reached the final step of the Redux flow.

That’s it. Hopefully this read gave you a better understanding of Redux’ architecture and how all data follows the same life cycle pattern. Although it adds another layer of abstraction to your code, Redux provides each component with the exact piece of state it needs.

Daniëlle Repko

Daniëlle Repko

Front-end Developer