Reducers

Reducers in NgRx are responsible for handling transitions from one state to the next state in your application. Reducer functions handle these transitions by determining which actions to handle based on the action's type.

Introduction

Reducers are pure functions in that they produce the same output for a given input. They are without side effects and handle each state transition synchronously. Each reducer function takes the latest Action dispatched, the current state, and determines whether to return a newly modified state or the original state. This guide shows you how to write reducer functions, register them in your Store, and compose feature states.

The reducer function

There are a few consistent parts of every piece of state managed by a reducer.

  • An interface or type that defines the shape of the state.

  • The arguments including the initial state or current state and the current action.

  • The functions that handle state changes for their associated action(s).

Below is an example of a set of actions to handle the state of a scoreboard, and the associated reducer function.

First, define some actions for interacting with a piece of state.scoreboard-page.actions.ts

content_copyimport { createAction, props } from '@ngrx/store';

export const homeScore = createAction('[Scoreboard Page] Home Score');
export const awayScore = createAction('[Scoreboard Page] Away Score');
export const resetScore = createAction('[Scoreboard Page] Score Reset');
export const setScores = createAction('[Scoreboard Page] Set Scores', props<{game: Game}>());

Next, create a reducer file that imports the actions and define a shape for the piece of state.

Defining the state shape

Each reducer function is a listener of actions. The scoreboard actions defined above describe the possible transitions handled by the reducer. Import multiple sets of actions to handle additional state transitions within a reducer.scoreboard.reducer.ts

content_copyimport { Action, createReducer, on } from '@ngrx/store';
import * as ScoreboardPageActions from '../actions/scoreboard-page.actions';

export interface State {
  home: number;
  away: number;
}

You define the shape of the state according to what you are capturing, whether it be a single type such as a number, or a more complex object with multiple properties.

Setting the initial state

The initial state gives the state an initial value, or provides a value if the current state is undefined. You set the initial state with defaults for your required state properties.

Create and export a variable to capture the initial state with one or more default values.scoreboard.reducer.ts

content_copyexport const initialState: State = {
  home: 0,
  away: 0,
};

The initial values for the home and away properties of the state are 0.

Creating the reducer function

The reducer function's responsibility is to handle the state transitions in an immutable way. Create a reducer function that handles the actions for managing the state of the scoreboard using the createReducer function.scoreboard.reducer.ts

content_copyconst scoreboardReducer = createReducer(
  initialState,
  on(ScoreboardPageActions.homeScore, state => ({ ...state, home: state.home + 1 })),
  on(ScoreboardPageActions.awayScore, state => ({ ...state, away: state.away + 1 })),
  on(ScoreboardPageActions.resetScore, state => ({ home: 0, away: 0 })),
  on(ScoreboardPageActions.setScores, (state, { game }) => ({ home: game.home, away: game.away }))
);

export function reducer(state: State | undefined, action: Action) {
  return scoreboardReducer(state, action);
}

Note: The exported reducer function is necessary as function calls are not supported the View Engine AOT compiler. It is no longer required if you use the default Ivy AOT compiler (or JIT).

In the example above, the reducer is handling 4 actions: [Scoreboard Page] Home Score, [Scoreboard Page] Away Score, [Scoreboard Page] Score Reset and [Scoreboard Page] Set Scores. Each action is strongly-typed. Each action handles the state transition immutably. This means that the state transitions are not modifying the original state, but are returning a new state object using the spread operator. The spread syntax copies the properties from the current state into the object, creating a new reference. This ensures that a new state is produced with each change, preserving the purity of the change. This also promotes referential integrity, guaranteeing that the old reference was discarded when a state change occurred.

Note: The spread operator only does shallow copying and does not handle deeply nested objects. You need to copy each level in the object to ensure immutability. There are libraries that handle deep copying including lodash and immer.

When an action is dispatched, all registered reducers receive the action. Whether they handle the action is determined by the on functions that associate one or more actions with a given state change.

Note: You can also write reducers using switch statements, which was the previously defined way before reducer creators were introduced in NgRx. If you are looking for examples of reducers using switch statements, visit the documentation for versions 7.x and prior.

Registering root state

The state of your application is defined as one large object. Registering reducer functions to manage parts of your state only defines keys with associated values in the object. To register the global Store within your application, use the StoreModule.forRoot() method with a map of key/value pairs that define your state. The StoreModule.forRoot() registers the global providers for your application, including the Store service you inject into your components and services to dispatch actions and select pieces of state.app.module.ts

content_copyimport { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import * as fromScoreboard from './reducers/scoreboard.reducer';

@NgModule({
  imports: [
    StoreModule.forRoot({ game: fromScoreboard.reducer })
  ],
})
export class AppModule {}

Registering states with StoreModule.forRoot() ensures that the states are defined upon application startup. In general, you register root states that always need to be available to all areas of your application immediately.

Last updated