320H.11 - Introduction to Redux

Learning Objectives

By the end of this lesson, learners will be able to:

  • Describe the purpose of Redux and the Redux Toolkit.
  • Explain when Redux should (or should not) be used.
  • Define key Redux terms.
  • Describe the use of createSlice.
  • Create a state slice with createSlice.
  • Use action creators provided by createSlice alongside the useDispatch hook to dispatch actions.
  • Use selector functions alongside the useSelector hook to retrieve data from the Redux store.
  • Create a simple application that integrates React and the Redux Toolkit.

 CodeSandbox

This lesson uses CodeSandbox as one of its tools.

If you are unfamiliar with CodeSandbox, or need a refresher, please visit our reference page on CodeSandbox for instructions on:

  • Creating an Account
  • Making a Sandbox
  • Navigating your Sandbox
  • Submitting a link to your Sandbox to Canvas

Prerequisite Knowledge

This lesson assumes that you have knowledge of general React state management principles, including reducer functions, dispatching actions, and React hooks.


Redux

As you continue scaling applications, the inevitable issue of state management looms on the horizon. Applications with large amounts of state can become difficult to manage, coordinate, organize, and debug. In React, you could have dozens of contexts at varying layers of the component tree, prop drilling, conplex inverse data flow, etc. Redux seeks to help manage these problems and more through the use of global state.

Redux is both a programming pattern and a library for managing and updating application state. It serves as a centralized store for state information that is used across an entire application. It also includes rules and logic that ensures that state can only be updated in a predictable manner.

While Redux can be used with any UI framework, this lesson will focus on its most common usage – alongside React. The React-Redux package integrates the two technologies.


Three Principles of Redux

Redux can be described in three fundamental principles:

  • Single Source of Truth

    • The global state of your application is stored in an object tree within a single store.
  • State is Read-Only

    • The only way to change the state is to emit an action, an object describing what happened.
  • Changes are made with Pure Functions

    • To specify how the state tree is transformed by actions, you write pure functions called reducers.

Benefits of Redux

Why should I use Redux?

Redux assists in the management of global state. It makes it easier to understand when, why, and how your application’s state is being updated, as well as how the application will behave when those changes occur. This helps ensure that your code is predictable and testable, increasing the likelihood that it will work as expected.

When should I use Redux?

Redux is not a one-size-fits-all solution to every state management problem. Redux is most useful when you have large amounts of application state that are needed across the entire application, when that state is updated frequently, when the logic to update that state may be complex, and when the application might be worked on by many people.


Similarity to React Hooks

You will see a lot of terms and techniques within Redux that look a lot like React's different hooks. Redux has its own hooks, and many of React's more modern features were put in place to accomplish the same tasks that Redux has accomplished. This similarity is intentional; learning one well will make it easier to use the other!


State Visualization with and without Redux

To gain a better understanding of what Redux does, consider this hypothetical component tree.

When a component initiates a change to the application’s state, and that state change affects many other components within the application, the process for passing that state change between components can get quite complicated (prop drilling, lifting state). Even with the use of React context, the question of where to put the context and how to subscribe to it can be confusing.

With Redux providing a centralized store for global state, as well as patterns and logic for handling state changes, the state-change process becomes much simpler and easier to manage.


The Tradeoffs of Redux

As with most toolkits, there are pros and cons to using Redux. The points above introduce some of the benefits, but it is important to note that Redux creates:

  • More concepts to learn.
  • More code to write.
  • Some indirection within the code.
  • Certain restrictions within the code.

Not all applications need Redux, so think carefully about your application’s requirements before applying Redux to it.


Getting Started with Redux

The Redux documentation includes an installation and setup guide to help you create your first React Redux application, which will be referenced here.

Step One: Install Redux Toolkit

Use the command line and your package manager of choice to install the Redux Toolkit.

npm install @reduxjs/toolkit

Step Two: Install React-Redux and Redux Developer Tools

Use the command line and your package manager of choice to install the React-Redux package and the Redux developer tools.

npm install react-redux
npm install --save-dev @redux-devtools/core

Step Three: Create a React Redux App

Use the command line to create a new app with React and Redux by using the official Redux JavaScript template for Create React App.

# Redux + Plain JavaScript
npx create-react-app my-app --template redux

# Redux + TypeScript
npx create-react-app my-app --template redux-typescript

Note that you can also create a TypeScript template if you are familiar with TypeScript.


It is important to note that create-react-app is now deprecated by the React team. Though it remains functional, developers are encouraged to use alternative tools and frameworks. Since these frameworks are beyond the scope of this course, we will continue to use create-react-app for our purposes.

We encourage exploration of other React frameworks and development tools like Next.js, Vite, and Remix as part of your learning efforts.


Redux DevTools

Another essential part of the Redux workflow, the Redux DevTools provide you with a way to debug your application’s state changes in the browser. The DevTools come as an extension for both Chrome and Firefox.

If you have not already done so, you should also install the React DevTools.


Redux Basics and Terminology

Here are some of the key terms you'll need to be familiar with in order to understand Redux. Some of these you will likely have already encountered, and others will be discussed in more detail as we progress:

  • Actions are plain JavaScript objects with a type field that describe something that happened within the application.
  • Reducers are functions that receive the current state and an action object, decide how to update the state based on the given action, and return the new state. Reducers are like event listeners, but instead of handling events, they handle actions.
  • The Redux store is where the application state lives.
  • Actions are dispatched to the store through store.dispatch(action), which is the only way to update the state in Redux. Dispatching actions in this way is similar to "triggering an event" within the application.
  • Selectors are functions that extract specific pieces of state information from the store.

Redux takes the entire global state of your application and places it in an object tree inside of a single store. The Redux store holds state in a protected way, disallowing you from changing it in any way other than dispatching actions (similarly to the React useReducer dispatch function).

Likewise, in order to describe how state should be updated in response to actions, you write pure reducer functions that calculate a new state based on the value of the old state and the action that was dispatched.

This pattern should look familiar from within React's hooks, but Redux implements several key differences along the way. First and foremost, Redux stores all global state in a single store, with a single reducer function. While this may sound like a lot of code in one file, there are ways to create more manageable slices of state logic.

Here's the fundamental, basic example of Redux logic and syntax provided by the documentation:

import { createStore } from 'redux'

/**
 * This is a reducer - a function that takes a current state value and an
 * action object describing "what happened", and returns a new state value.
 * A reducer's function signature is: (state, action) => newState
 *
 * The Redux state should contain only plain JS objects, arrays, and primitives.
 * The root state value is usually an object. It's important that you should
 * not mutate the state object, but return a new object if the state changes.
 *
 * You can use any conditional logic you want in a reducer. In this example,
 * we use a switch statement, but it's not required.
 */
function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + 1 }
    case 'counter/decremented':
      return { value: state.value - 1 }
    default:
      return state
  }
}

// Create a Redux store holding the state of your app.
// Its API is { subscribe, dispatch, getState }.
let store = createStore(counterReducer)

// You can use subscribe() to update the UI in response to state changes.
// Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly.
// There may be additional use cases where it's helpful to subscribe as well.

store.subscribe(() => console.log(store.getState()))

// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
store.dispatch({ type: 'counter/incremented' })
// {value: 1}
store.dispatch({ type: 'counter/incremented' })
// {value: 2}
store.dispatch({ type: 'counter/decremented' })
// {value: 1}

In the above basic counter example, we see some familiar and some unfamiliar terms and syntax:

  • The counterReducer function is a reducer function that takes two arguments: the current state, and the action that was dispatched. It uses a switch statement to determine logic based on the action's type, and returns a new state value.
  • createStore is called to create the Redux store that holds the state of the application.
  • Actions are .dispatched to the store in order to modify the state based on the result of the reducer function.

Instead of mutating state directly, you always specify mutations with action objects in Redux.

The single reducer function that controls the entire application's state, called the root reducer, is often split into smaller independant reducers that operate on separate parts of the state tree, which is why you see actions like "counter/incremented" instead of just "incremented"; the action objects describe both the action to be performed and the piece of state it was applied to.

The way these pieces of state logic are broken apart mimics the way React's component tree works - there is a single root component, but it is composed of many smaller components that are included within it.

While you wouldn't necessarily want to use Redux for a simple counter application like the example above, the benefits of Redux are numerous when scaling into large applications.

Here's a graphical representation of Redux's data flow:

redux data flow

The Redux Toolkit

If those patterns weren't simple enough, modern Redux makes use of the Redux Toolkit (RTK), a package intended by the Redux team to be the standard way of writing Redux logic. It addresses the following concerns about core Redux:

  • "Configuring a Redux store is too complicated"
  • "I have to add a lot of packages to get Redux to do anything useful"
  • "Redux requires too much boilerplate code"

While RTK doesn't solve every problem for every user, it does provide tools that abstract a lot of the setup processes and solve some of the more common use cases for Redux. The Redux team recommends that all users of Redux write using RTK, from first-time learners to veteran developers.

The Redux Toolkit...

  • Simplifies store setup down to a single clear function call, while retaining the ability to fully configure the store's options if you need to.
  • Eliminates accidental mutations, which have always been the #1 cause of Redux bugs.
  • Eliminates the need to write any action creators or action types by hand.
  • Eliminates the need to write manual and error-prone immutable update logic.
  • Makes it easy to write a Redux feature's code in one file, instead of spreading it across multiple separate files.
  • Offers excellent TypeScript support, with APIs that are designed to give you excellent type safety and minimize the number of types you have to define in your code.

Here is the same counter app from above, written with the Redux Toolkit:

import { createSlice, configureStore } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    incremented: state => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decremented: state => {
      state.value -= 1
    }
  }
})

export const { incremented, decremented } = counterSlice.actions

const store = configureStore({
  reducer: counterSlice.reducer
})

// Can still subscribe to the store
store.subscribe(() => console.log(store.getState()))

// Still pass action objects to `dispatch`, but they're created for us
store.dispatch(incremented())
// {value: 1}
store.dispatch(incremented())
// {value: 2}
store.dispatch(decremented())
// {value: 1}

While this example is obviously shorter and easier to read, it also contains many beneficial changes that will help us write Redux logic and scale our applications (note the reference to Immer!). We will discuss all of these in detail during this and future lessons, but first let's discuss the most important new feature: createSlice.


Creating Slices of State

We create Redux "state slices" with the createSlice API from RTK. Creating a slice involves specifying a name to identify the slice, an initialState value, and at least one reducer function to define how the state can and should be updated. Once the slice has been created, it automatically generates a variety of very useful tools that can be exported for use elsewhere.

Breaking down the counter example above, we...

  1. Import createSlice:
import { createSlice, configureStore } from '@reduxjs/toolkit'
  1. Assign a name:
const counterSlice = createSlice({
  name: 'counter',
  ...
  1. Provide initialState:
const counterSlice = createSlice({
  ...
  initialState: {
    value: 0
  },
  ...
  1. Create Reducers:
import { createSlice, configureStore } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  ...
  reducers: {
    incremented: state => {
      state.value += 1
    },
    decremented: state => {
      state.value -= 1
    }
  }
})
  1. Add the Slice Reducers to the Store:
const store = configureStore({
  reducer: counterSlice.reducer
})

React Redux Example

While the code above is a valid example of the basic pattern, it lacks scalability. In order to demonstrate how slices are typically created, and how files are managed across a React Redux application, let's look at a working example of this counter, modified from the Redux documentation:

There are many things to take note of here, starting with the organizational structure of the application.

While different teams take different approaches to organization, this one is very common. Each "feature" of the application, in this case our counter, is kept in its own folder within a "features" directory. Inside, you can find the React component (Counter.js), the styling for the component (Counter.module.css), and the state slice for the component (counterSlice.js). Keeping these files together makes it very clear what affects what, and allows you to quickly make changes to individual features of the application.

Within counterSlice.js, we create a state slice that then exports three important items: increment, decrement, and selectCount.

createSlice automatically creates "action creators" for us. Action creators are simply object factories that dispatch specific actions, for consistency. Instead of calling something like store.dispatch({ type: 'counter/incremented', payload: 1 }), we can simply call dispatch(increment) thanks to RTK.

Similarly, the selectCount "selector function" gives us the specific piece of state information that this slice works with, allowing us to easily access that information elsewhere without having to know the exact structure of the state. Selector functions take the current state as an argument, and return a piece of that state (or a result of state calculations) based on the logic within the function.


React Redux Hooks

Like React, Redux contains many hooks (thanks to react-redux, which we will talk in detail about during a later lesson). These hooks allow us to access specific parts of RTK functionality without the need for verbose syntax or complicated logic.

Within the Counter.js file, which contains the Counter component, we make use of the useSelector and useDispatch hooks from react-redux, and the decrement, increment, and selectCount functions from our counterSlice.

useSelector takes a single selector as an argument and returns the result of that selector. Any time state is updated, the value of this result is also updated to reflect changes (if any) to the state that it uses.

useDispatch takes no arguments, and simply provides access to the dispatch function from anywhere in the application.

With these tools, displaying the value of our current count is as simple as {count}, and modifying it is as simple as assigning onClick={() => dispatch(increment())}.

If this process seems confusing or complicated at this stage, that is okay! We will continue to build upon examples of the Redux pattern and examine its syntax in more detail as we continue. It is important, however, to start with a basic understanding of how the Redux Toolkit works from the perspective of a complete application.


Building Features with RTK

In order to demonstrate the way features are built, let's continue adding onto the counter application.

As with a typical counter example, we want to have the option for the user to increment or decrement by a value of their choosing. Let's add an input element to the structure of our Counter component to do so:

<input
  type="number"
  min="1"
  step="1"
  className={styles.numinput}
  aria-label="Set increment amount"
/>

In order to track the value of this input element, we need to create a new piece of state and turn the input into a controlled input by binding its value to state and creating an onChange handler that updates that state.

Before we do so, ask yourself: where else will this state be used? This particular piece of state is entirely local to the Counter component itself. It will never be used elsewhere. This means that it does not need to be added to the Redux global state store, which is for global state!

You can mix React local state and Redux global state as much as you need to within your applications, and doing so is considered good practice! Each state management solution has its place, and putting everything into your Redux store is almost never the correct approach!

import { useState } from "react";
...
const [incrementAmount, setIncrementAmount] = useState(1);
...
<input
  ...
  value={incrementAmount}
  onChange={e => setIncrementAmount(e.target.value)}
/>

Now that we have this value in state, we need a way to send it alongside our dispatched actions. With Redux Toolkit, doing so is as simple as adding the value we want to send as the argument to the action creators given by createSlice.

dispatch(increment(incrementAmount))
...
dispatch(decrement(incrementAmount))

Here's how the Counter.js file looks so far:

Notice, however, that it does not currently work as intended. This is because we haven't yet added logic to our reducer functions to handle the new information!

Redux Toolkit automatically sends state as the first argument to reducers, and action as the second one. We can therefore modify our reducer functions in counterSlice.js like so:

increment: (state, action) => {
  state.value += action.payload;
},
decrement: (state, action) => {
  state.value -= action.payload;
}

If you test this, however, you'll notice that something is wrong. By default, the number input still returns a string, resulting in string concatenation instead of numeric addition and subtraction! There are a number of ways to fix this, but the simplest is wrapping the set value in Number().

While we're at it, let's also add a "reset" button that resets the increment input value to 1 and the counter's value to 0. Try handling this yourself before taking a look at the completed solution below!

These are the basic, repeatable steps to creating interactive components using the Redux Toolkit with React. While there is much more to discuss and many more features to reveal, you now have the general knowledge required to begin integrating Redux into your React applications... almost.


The Redux Provider

While working with the live example, we never explained how to use Redux with React to begin with. It's a concept that will seem very familiar, and you may have already noticed it when exploring the example application's file system: Provider.

The react-redux package contains many integration tools that allow Redux to work hand-in-hand with React. We will discuss many of them in later lessons, but Provider is required to provide the Redux store to your React application.

Take a look in the index.js file of the example application and you will see the following:

import { createRoot } from "react-dom/client";
import "../styles/index.css";
import App from "./App";
import store from "./app/store";
import { Provider } from "react-redux";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

Here, we do everything we need to do to set up the application. Aside from the typical React setup, we also see a <Provider> component being imported from react-redux, and the store being imported from our app/store.js file.

Just like the useContext hook in React, here we wrap the <App> in the <Provider> that delivers the store to all of the internal components.

If you don't do this, the application will have no idea what store you're talking about!


Global State Management

As a last piece of food for thought, ask yourself: is this how you would build a simple counter application?

The answer should be no! At least with the current state of our example application, there is no reason to use Redux. Everything we've done could have been accomplished using React's hooks and local state management. If you look at where we use the counter state, you'll notice that it is only ever used in the Counter component! This is a prime example of the use case for local state.

However, the scale at which an application tends to benefit from Redux does not lend itself well to examples for the purpose of learning. As we continue, we will provide more small, disgestible example applications like this one in order to demonstrate the core functionality of the Redux Toolkit. It is up to you to decide whether or not Redux is helpful or necessary in your future projects!

And don't forget: you can mix global Redux state management with local React state management!


Practice Activity: Making Use of Global State

Fork the CodeSandbox counter example above, and create a new, simple component within App.js. The component does not need to be complex or have any styling, it just needs to display the value of the counter state outside of the Counter component.

Since Redux state is global, it should be accessible from anywhere without too much effort. Give it a try!


Further Learning

As you continue your journey into Redux, remember that the official documentation contains most of the information you will need to be successful, and it even comes with a section on frequently asked questions.

Within the official documentation, there are also many additional learning resources created and made available by the Redux community.


A React + Redux Application: Part One

GLAB 320.11.1 - Redux State Management is a four-part guided lab excercise designed to introduce and reinforce the core concepts and tools of the Redux Toolkit by having you build a fully-working React Redux application with complex state.

Please complete part one of this lab, and save the remaining parts for after future lessons.

Copyright © Per Scholas 2024