Skip to main content

Chapter 6: Reducers

How do we modify the State?#

So far, we have learned how to create Actions, the building blocks of NgRx, that signal the Store about what happened in the application and what needs to be done. But Actions are just objects, that don't hold any functionality; they by themselves cannot make any State changes. To make those changes actually happen, we have to use a Reducer, next of the core most important concepts of NgRx.

What are Reducers?#

Reducers are pure functions that receive two arguments, the current State of the application, an Action object, calculate the new state based on the Action that happened, and return that new State. Usually calculating involves determining which Action happened (sometimes via switch-case statements, but NgRx provides utilities that reduce boilerplate), copying the previous state into a new object while modifying the relevant properties of the State, and returning the new State object. Every time an Action is dispatched (we will learn more about dispatching in the next chapters, for now "dispatching" an Action means the Action has happened), NgRx will call the Reducer function, providing the old State and the Action object as arguments, get the returned new State and immediately notify all of our components about the change (how neat is that!). Essentially, Reducers are the central place where State changes happen.

What are pure functions?#

In the previous paragraph, we mentioned a concept of "pure functions", a core principle of functional programming. If you are familiar with the concept of a pure function, or with functional programming in general, you can skip this paragraph. If not, let's explore what a pure function is. A pure function is a function that:

  1. Receives data only using its arguments
  2. Never accesses global variables
  3. Provided the same arguments, always returns the same result
  4. Does not have side effects. By side effects we mean every impact a function can have on the external environment, like modifying a global variable, console.log-ing something, sending HTTP requests, accessing localStorage or cookies and so on.

Basically, a pure function is a function that only receives arguments, calculates a result (that is always the same for the same arguments), and returns that result.

Example of a pure function:

function add(a: number, b: number) {  return a + b;}

This function will always return the same result for the same numbers, and has zero impact on the external environment.

Example of an impure function:

function sendRequest(url: string) {  return fetch(url).then((response) => response.json());}

This function makes an HTTP request, which is a side effect. On top of that, even if the URL stays the same, there is no guarantee that the remote server will always return the same data; thus, this function is impure.

Pure functions are easy to understand, test and debug. Thus, they are a very important concept in functional programming, and, by extension, NgRx. You can read more about pure functions here and here.

Understanding our State#

Before we can write a Reducer, we first must understand the shape of our State. What data it holds, what are the property names and so on. So far our state is simple: we have an array of Category items, and that is it.

In the state folder that we have created, create a file named state.ts and put the following code in it:

export interface Category {  name: string;}
export interface AppState {  categories: Category[];}
export const initialState: AppState = {  categories: [],};

In this file, we have defined what a Category is, defined how our application State looks via the AppState interface, and defined the initialState (the state that the app has before any Action has happened). Now we are set to create our first Reducer function!

Writing Reducers#

In the same state folder, create another file named reducer.ts, and put the following code in it:

import { Action } from "@ngrx/store";
import { AppState } from "./state";
export function reducer(state: AppState, action: Action) {  switch (action.type) {    case "[Category List] Add Category":      return { ...state, categories: [...state.categories, action.payload] };    default:      return state;  }}

NOTE: Beware of the payload attribute of the Action, which only fits the "old" way to create actions. Next example will use the props and call directly the properties added to the Action.

Now let's deconstruct this example. First of all, as you see, the reducer function is a pure function. Next, it differentiates between Actions using a switch statement and the Action types. On each different Action, our Reducer will return a slightly different version of a State to match that Action's intent. As we have only one Action for now, we wrote only one case statement, and if the Action is not identified, we return the state without modifying it. As you can see, the practice is to copy the state, and return a new object. Never modify the previous state object, always return a new one. What will happen is when we dispatch the addCategory Action, the reducer function will be called, the new State (with the added category object in the categories array) will be returned, and then NgRx will propagate that change to all of our components.

Reducer is a great way to centralize State changes. if we have any problems with how our State has been changed, we need only to look inside the reducer to find out where the bug is. It is a pure function, so it also easy to test, and understand.

Using NgRx built-in functions to reduce boilerplate#

Writing long switch statements can be tedious; those might be even harder to read; a Reducer in a large enterprise project might as well have hundreds of case statements. Thus, the NgRx team has created special functions to help us generate the Reducer function. Let's rewrite our code to use those functions:

import { Action, createReducer, on } from "@ngrx/store";
import * as actions from "./actions";import { AppState, initialState } from "./state";
const _reducer = createReducer(  initialState,  on(actions.addCategory, (state, {category}) => ({    ...state,    categories: [...state.categories, category],  })));
export function reducer(state: AppState, action: Action) {  return _reducer(state, action);}

Let's understand what goes on here. First of all, we have the createReducer function, which does exactly that - creates the resulting Reducer function. It receives the initial state of the application, and then an arbitrary amount of handlers for each action (we provided only one, but we can (and will!) have lots of Actions, and thus. lots of on function calls). The on function receives an Action as the first argument, and a callback function as a second one, which acts like a mini-Reducer for only that specific Action. Thus, the need to write huge switch statements is eliminated. Note that the callback signature is (state, {category}) in which we destruct the action passed in the second argument in order to only get the props. Now let's understand two specific parts of this code:

  1. Why did we create a _reducer function only to call it in the reducer function? The thing is, in the next chapters we are going to register the reducer function in our AppModule, and because of how the Angular build process works, it does not allow including anonymous functions like the one returned by createReducer, only declared named functions. Thus we wrote a conventional function and called our _reducer from it. Always create Reducers like this.
  2. _Why didn't we import the Action we wanted by name, but rather did import * as actions from './actions';? Because we can easily have dozens of Actions that are handled in a single Reducer function, we would like to import them as a single object to avoid too cluttered imports.

Homework#

Of course, homework isn't going anywhere. Remember the Actions we created in the previous homework? we are going to use them.

  1. In the same reducer, create a handler for deleting a Category (consider the name of the Category unique).
  2. Write a handler that deletes all categories from the Store.

Now we not only know how to create Actions modify the State with them in the Reducer function, but also how to avoid too much boilerplate code. Now let's learn how to actually use the State data in our components.

Exercise 1 solution
reducer.ts
const _reducer = createReducer(  initialState,  // other handlers  on(actions.deleteCategory, (state, {name}) => ({    ...state,    categories: state.categories.filter(      (cat) => cat.name !== name    ), // filter out the deleted category  })));
Exercise 2 solution
reducer.ts
const _reducer = createReducer(  initialState,  // other handlers  on(actions.deleteAllCategories, (state) => ({ ...state, categories: [] })) // just assign a new empty array for categories);