Create a Reducer page

Learn how to create an NgRx Reducer.

Quick Start: You can checkout this branch to get your codebase ready to work on this section.

Overview

  1. Define Login State shape.

  2. Set Login initial state.

  3. Update Reducer function to include handler for LoginActions.loginSuccess Action to update Login State.

  4. Update Reducer function to include handler for LoginActions.logoutSuccess Action to reset Login State.

Problem 1: Define Login State Shape

Update the Login State shape by adding userId, username, and token properties to its interface definition. Each property should have type string | null.

P1: What you need to know

Now that we have all of our Actions prepared to dispatch whenever we need, we will update the Login State. Typically a Reducer is updated with 3 steps:

  1. Define or update Store interface to have an expected shape after Reducer updates state.

  2. Set or update value to state’s initial value to satisfy new Store interface definition.

  3. Add or update handler(s) used to define Reducer function.

To prepare this, we need to update the Login Feature State interface found at src/app/store/login/login.reducer.ts. Here is an example of how to add properties to an interface:

// Note: This example code is not part of our application repo or solution

export interface Contact {
    emailAddress: string | null;
    fullName: string | null;
}

P1: Solution

src/app/store/login/login.reducer.ts

// src/app/store/login/login.reducer.ts

import { createReducer } from '@ngrx/store';

export const loginFeatureKey = 'login';

export interface State {
  userId: string | null;
  username: string | null;
  token: string | null;
}

export interface LoginPartialState {
  [loginFeatureKey]: State;
}

export const initialState: State = {

};

export const reducer = createReducer(
  initialState
);

Problem 2: Set Initial Value For Login State

Set initial state for Login State. Each member of the Login State should start with null as its value.

P2: What you need to know

Now that we have updated the Login State’s shape by updating the State interface, we need to update its initial shape. By default, we will set each member to null. Here is an example of doing that:

export const initialValue: Contact = {
    emailAddress: null;
    fullName: null;
}

P2: Solution

src/app/store/login/login.reducer.ts

// src/app/store/login/login.reducer.ts

import { createReducer } from '@ngrx/store';

export const loginFeatureKey = 'login';

export interface State {
  userId: string | null;
  username: string | null;
  token: string | null;
}

export interface LoginPartialState {
  [loginFeatureKey]: State;
}

export const initialState: State = {
  userId: null,
  username: null,
  token: null,
};

export const reducer = createReducer(
  initialState
);

Problem 3: Update Login State With Login Information On Login

Login Reducer should include an on() handler that updates Login State with userId, username and token whenever LoginActions.loginSuccess Action is dispatched.

P3: What you need to know

We can create NgRx Reducers using the createReducer() helper function.

The first argument for createReducer() sets the initial value of your State. Then every argument after should be an on() handler function calls.

When writing an on() handler, there are 2 arguments that we need to provide:

  1. The Action that this on() handler is reacting to.

  2. A pure function that takes in 2 arguments: state and action. This function should always return a new state that will replace the previous state.

// Note: This example code is not part of our application repo or solution

import { createReducer, on } from '@ngrx/store';
import * as CounterActions from './counter.actions';

export const scoreboardReducer = createReducer(
  initialState,// Initial feature state
  on(CounterActions.set, (state, action) => ({ ...state, count: action.count })),// Set counter
  on(CounterActions.increment, state => ({ ...state, count: state.count + 1 })),// Increment counter
  // ...
);

P3: Solution

src/app/store/login/login.reducer.ts

// src/app/store/login/login.reducer.ts

import { createReducer, on } from '@ngrx/store';
import * as LoginActions from './login.actions';

export const loginFeatureKey = 'login';

export interface State {
  userId: string | null;
  username: string | null;
  token: string | null;
}

export interface LoginPartialState {
  [loginFeatureKey]: State;
}

export const initialState: State = {
  userId: null,
  username: null,
  token: null,
};

export const reducer = createReducer(
  initialState,
  on(
    LoginActions.loginSuccess,
    (state, { userId, username, token }): State => ({
      ...state,
      userId,
      username,
      token,
    })
  )
);

Problem 4: Reset Login State On Logout

Login Reducer should include an on() handler that resets Login State back to initialState whenever LoginActions.logoutSuccess Action is dispatched.

P4: What you need to know

A common requirement is to reset state. One approach might look like this:

// Note: This example code is not part of our application repo or solution

import { createReducer, on } from '@ngrx/store';
import * as CounterActions from './counter.actions';

export const scoreboardReducer = createReducer(
  initialState,// Initial feature state
  on(CounterActions.set, (state, action) => ({ ...state, count: action.count })),// Set counter
  on(CounterActions.increment, state => ({ ...state, count: state.count + 1 })),// Increment counter
  on(CounterActions.reset, (state) => ({ ...state, count: 0 })),// Reset counter
  // ...
);

But there is nothing wrong with reusing the initialState constant:

// Note: This example code is not part of our application repo or solution

import { createReducer, on } from '@ngrx/store';
import * as CounterActions from './counter.actions';

export const scoreboardReducer = createReducer(
  initialState,// Initial feature state
  on(CounterActions.set, (state, action) => ({ ...state, count: action.count })),// Set counter
  on(CounterActions.increment, state => ({ ...state, count: state.count + 1 })),// Increment counter
  on(CounterActions.reset, () => ({ ...initialState })),// Reset counter
  // ...
);

Both solutions are fine, but it is likely better to reuse initialState to future-proof the Reducer for future requirements.

P4: Solution

src/app/store/login/login.reducer.ts

// src/app/store/login/login.reducer.ts

import { createReducer, on } from '@ngrx/store';
import * as LoginActions from './login.actions';

export const loginFeatureKey = 'login';

export interface State {
  userId: string | null;
  username: string | null;
  token: string | null;
}

export interface LoginPartialState {
  [loginFeatureKey]: State;
}

export const initialState: State = {
  userId: null,
  username: null,
  token: null,
};

export const reducer = createReducer(
  initialState,
  on(
    LoginActions.loginSuccess,
    (state, { userId, username, token }): State => ({
      ...state,
      userId,
      username,
      token,
    })
  ),
  on(LoginActions.logoutSuccess, (): State => ({ ...initialState }))
);

Wrap-up: By the end of this section, your code should match this branch. You can also compare the code changes for our solution to this section on GitHub or you can use the following command in your terminal:

git diff origin/create-reducer