Introduction
Actions, Reducers and Effects are building blocks in Ngrx. They are used in many Angular applications. This article explains the relationship between the three and their uses an application
Actions
Actions are one of the main building blocks in Ngrx. Actions express unique events that happen throughout an application. The 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 take a look at what an Action interface looks like.
interface Action {
type: string
}
The Action interface has a single property. Its type is represented as a string. It describes the action that will be dispatched into an application. Its value 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 more context or metadata . Actions are JavaScript objects in simple terms.
An event is triggered from an authentication after interacting with a backend API. It can be described as
{
type: '[Auth API] Login success';
username: string;
password: string;
phone - number: number;
}
The above action is an event triggered by a user clicking on a login button from the login page. to attempt to authenticate a user. The username, password and phone-number are defined as extra 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. More detailed information that can be used to debug in the developer tools should be added.
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';
Next we import our data source
import { Assessmentconfig } from 'src/app/shared/models/school.model';
export enum AssessmentconfigActionTypes {
CreateAssessmentconfig = '[Assessmentconfig] Create'
}
Next we put in place our action
export class CreateAssessmentconfig implements Action {
type = AssessmentconfigActionTypes.CreateAssessmentconfig;
constructor(public payload: { assessmentconfig: Assessmentconfig }) {}
};
The CreateAssessmentconfig function returns an object in the shape of an action interface. The constructor will be used to define 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 then be used to return the action when dispatching.
onSubmit(username: string, password: string){
store.dispatch(CreateAssessmentconfig({
username: username,
password: password
}
))
};
The CreateAssessmentconfig action creator receives an object of username and password. It 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 the sense that they produce the same output for a given input. They do this without any side effects, handling state transition synchronously. Each reducer function takes the latest Action dispatched to the current state. It determines whether to return a 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;
}
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 }) {}
};
The Actions describes the transitions that is 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
}
});
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;
}
}
}
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. They return a new state of objects using the spread operator. The spread operator copies the properties for the current state into the object. This creates a new reference.
It ensures that a new state is produced with the change. The purity of the change is preserved. It promotes referential integrity that guarantees old references are discarded in the occurence of state change. 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. It allows for more pure components that select state and dispatch actions.
- Effects are long running services that listen to observable of every action dispatched on the store
- Effects filter the actions based on the type of action they are interested in. This is done by an operator
- Effects 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. This allows services to be less stateful only performing tasks related to 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 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. It 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';
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';
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) {}
}
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.
Top comments (0)