DEV Community

loading...
Cover image for Understanding Ngrx Actions, Reducers and Effects

Understanding Ngrx Actions, Reducers and Effects

Muhammad Muhktar Musa
javaScript enthuthiast
Updated on ・6 min read

Introduction

Actions, Reducers and Effects are building blocks in Ngrx which is used in many Angular applications. This article is meant to explain the relationship between the three and how we can use them in an application

Actions

Actions are one of the main building blocks in Ngrx. Actions express unique events that happen throughout an application. This events can be user interaction with a page, external interaction through network request and direct interaction with the device API's. Actions are the input and output of many systems in Ngrx. They help in understanding how events are handled in an application. The Action is an object like interface. Let us have a look of what an Action interface looks like.

interface Action {
  type: string
}
Enter fullscreen mode Exit fullscreen mode

The Action interface has a single property. Its type is represented as a string. The type property is for describing the action that will be dispatched into an application. The value of the type comes from the [source] event and is used to provide a context of what category of action is to be taken. Properties are added to an action to provide additional context or metadata for an action. Actions are JavaScript objects in simple terms. For example an event is triggered from an authentication after interacting with a backend API can be described as

{
  type: '[Auth API] Login success';
  username: string;
  password: string;
  phone - number: number;
}
Enter fullscreen mode Exit fullscreen mode

The above action is an event triggered by a user clicking a login button from the login page to attempt to authenticate a user. The username, password and phone-number are defined as additional metadata from the login page.

Writing Actions

The following rules should be applied when a good action is to be written within an application

  • Write actions before developing features. This is to understand and gain a shared knowledge of the feature being implemented

  • Provide contents that are descriptive and that are targeted to a unique event with more detailed information that can be used to debug in the developer tools.

  • Divide actions into categories based on the event source.

  • Actions are inexpensive to write. For this reason, the more actions written the better a developer can express a work flow.

  • Actions should be event driven. Events should be captured and not commands as the description of an event are the handling of the event.

Let us take a look at an example Action. First we import Actions from the Ngrx store into our action file

import { Action } from '@ngrx/store';
Enter fullscreen mode Exit fullscreen mode

Next we import our data source

import { Assessmentconfig } from 'src/app/shared/models/school.model';

export enum AssessmentconfigActionTypes {
  CreateAssessmentconfig = '[Assessmentconfig] Create'
}
Enter fullscreen mode Exit fullscreen mode

Next we implement our action

export class CreateAssessmentconfig implements Action {
 type = AssessmentconfigActionTypes.CreateAssessmentconfig;
  constructor(public payload: { assessmentconfig: Assessmentconfig }) {}
};
Enter fullscreen mode Exit fullscreen mode

The CreateAssessmentconfig function returns an object in the shape of an action interface. The constructor will be used to define any additional metadata needed for the handling of the action. The action being dispatched should be created in a consistent, type-safe way. The action creator can the be used to return the action when dispatching.

onSubmit(username: string, password: string){
  store.dispatch(CreateAssessmentconfig({
    username: username,
    password: password
  }
  ))
};

Enter fullscreen mode Exit fullscreen mode

The CreateAssessmentconfig action creator receives an object of username and password and returns a plane javaScript object with a property type of [Login Page], Login. The returned action has very specific context about where the action came from and what happened.

  • The category of the action is captured within the square brackets []
  • The category is used to group actions for a particular area. This area can be a component page, backend API or browser API
  • The Login text after the category is a description of what event occurred from the action.

Reducers

Reducers are functions responsible for handling transitions from one state to the next state in an application. They are pure functions in that they produce the same output for a given input without any side effects handling 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.

The Reducer Function

The consistent parts of pieces of state managed by a reducer are

  • An interface or type that defines the shape of the state
  • The functions that handle the state changes for the associated actions
  • The arguments including the initial state or current state and current action.

Let us take a look at an example

export interface AssessmentconfigState {
  // additional entities state properties
  selectedId: number;
  loading: boolean;
  error: string;
  query: AssessmentconfigSearchQuery;
}
Enter fullscreen mode Exit fullscreen mode

A reducer file is created and the a default state is set as in above. A reducer function is a listener of actions.

export class CreateAssessmentconfig implements Action {
 type = AssessmentconfigActionTypes.CreateAssessmentconfig;
  constructor(public payload: { assessmentconfig: Assessmentconfig }) {}
};
Enter fullscreen mode Exit fullscreen mode

The above code in the Actions folder describes the transitions that will be handled by the reducer. We will import this action into the reducer file. The shape of the state will now be defined according to what is to be captured.

We can now use the default state to create an initial state for a required state property.

export const initialAssessmentconfigState: AssessmentconfigState({

  selectedId: null,
  loading: false,
  error: '',
  query: {
    filter: '',
    sorting: '',
    limit: 999,
    page: 1
  }
});
Enter fullscreen mode Exit fullscreen mode

To create a reducer function we can

export function assessmentconfigReducer(state = initialAssessmentconfigState,
  action: AssessmentconfigActions): AssessmentconfigState {
  switch (action.type) {
    case AssessmentconfigActionTypes.CreateAssessmentconfig:
      return {
        ...state,
        loading: true,
        error: ''
      };

    default:
      return state;
  }
}
}
Enter fullscreen mode Exit fullscreen mode

In the reducer above, the action is strongly typed. The action handles the state transition immutably. The state transition are not modifying the original state but returning a new state of objects using the spread operator. The spread syntax copies the properties for the current state into the object creating a new reference.
This ensures that a new state is produced with the change. This preserves the purity of the change thereby promoting referential integrity that guarantees old references are discarded when a state change occurs. When an action is dispatched, all registered reducers receive the action. Reducers are only responsible for deciding which state transition should occur for a given action.

Effects

In an Angular application there is need to handle impure actions. Impure actions can be network request, websocket messages and time based events. In a service based Angular application, components are responsible for interacting with external resources through services. Effects provide a way to interact with those services so as to isolate them from the component. They handle task such as fetching data, running task that produce multiple events and other external interactions where components do not need explicit knowledge of such interactions. In other words effects

  • isolate side effects from components allowing for more pure components that select state and dispatch actions.
  • are long running services that listen to observable of every action dispatched on the store
  • filter the actions based on the type of action they are interested in. This is done by an operator
  • performs tasks which are synchronous or asynchronous, returning a new action.

In service based applications, components interact with data through many different services that expose the data through properties and methods. This services may depend on other services. Components consume these services to perform task giving them many responsibilities.
Effects when used along with the store decreases the responsibility of the component. In a larger application, it becomes more important because of multiple sources of data. Effects handle external data and interactions allowing services to be less stateful and only perform tasks related to the external interactions.

Writing Effects

To isolate side effects from a component, an effect class should be created to listen for events and perform task. Effects are injectable service classes with distinct parts which are

  • An injectable actions service that provides an observable stream of all actions dispatched after the latest state has been reduced.
  • Metadata is attached to the observable stream using the create function. The metadata is used to register the streams the store subscribes to and returns actions from the effects stream dispatching back to the store.
  • Actions are filtered using pipeable ofType operator. This operator takes one or more action types as arguments and filters the action to be acted upon.
  • Effects are subscribed to the store observable.
  • Services are injected into effects to interact with external API's and handle stream.

Let us take an example at play
First we import

import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
Enter fullscreen mode Exit fullscreen mode

The action and services are now imported from the

import {AssessmentconfigActionTypes,CreateAssessmentconfig,
 CreateAssessmentconfigFail 
} from './assessmentconfig.actions';
import { SchoolService } from 'src/app/shared/services/school.service';
Enter fullscreen mode Exit fullscreen mode

We create the effects by

@Injectable()
export class AssessmentconfigEffects {
  // ========================================= CREATE
  @Effect()
  create: Observable<Action> = this.actions$.pipe(
    ofType<CreateAssessmentconfig>(AssessmentconfigActionTypes.CreateAssessmentconfig),
      this.service.createAssessmentConfig(schoolId, action.payload.assessmentconfig).pipe(
        switchMap((assessmentconfig: Assessmentconfig) => {
          const a1 = new CreateAssessmentconfigSuccess({ result: assessmentconfig });
          const a2 = new SelectAssessmentconfigById({ id: assessmentconfig.id });
          return [a1, a2];
        }),
        catchError(({ message }) => of(new CreateAssessmentconfigFail({ error: message })))
      )
    )
  );

  constructor(private actions$: Actions, private store: Store<ApplicationState>,
     private service: SchoolService) {}
}
Enter fullscreen mode Exit fullscreen mode

The AssessmentconfigEffects is listening for all dispatched actions through the Action stream. It shows its specific interest by using the ofType operator. The stream of action is then mapped into a new observable using the switchMap operator. It returns a new action with an error method attached. The action is dispatched to the store where it would be handled by the reducers when a state change is needed. It is very important to handle errors when dealing with observable streams so that the effects can continue running.

This brings us to the end of this article. We have explained how to create Ngrx Actions, Reducers and Effects as well as their implementation in an application.

Discussion (0)