Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

Handling side effects in an Angular + Redux application

5 min read 1545

Handling Side Effects in a Redux and Angular Application

Introduction

With Redux gaining widespread popularity within the frontend ecosystem, Angular and other leading frontend frameworks have embraced it as one of their reliable state management libraries.

But unfortunately, the Redux architecture doesn’t provide any built-in functionality for handling asynchronous data changes (also known as side effects) to the Redux state tree. As a result, when these asynchronous actions are done, the Redux state tree will be affected.

This article will introduce the @ngrx/effects library, a special package for handling side effects in NgRx applications, and how it can be used to handle side effects in NgRx applications.

Prerequisites

  1. Knowledge of Angular
  2. Knowledge of NgRx
  3. Knowledge of TypeScript

What are side effects?

Side effects are operations such as fetching data from a remote server, accessing local storage, recording analytics events, and accessing files that usually finish sometime in the future.

Knowing what side effects are, let’s say we want to make a request to an API endpoint to fetch a list of users in our app. Considering the fact that such an operation will be asynchronous, the following cases will be accounted for:

  1. FETCHING_USERS
  2. USERS_FETCH_SUCCESSFUL
  3. ERROR_FETCHING_USERS

Let’s get our hands dirty by doing some practice.

Environment setup

Create a new Angular project with the following command:

ng new side-effects

Run the following command to install the required dependencies for this exercise:

npm install --save @ngrx/effects @ngrx/store rxjs

Next, we will run the following command to create a feature module for users:

ng generate module user

Then, we will create a constants.ts file to hold FETCHING USERS, USERS FETCH SUCCESSFUL, and ERROR FETCHING USERS as follows:

We made a custom demo for .
No really. Click here to check it out.

//src/app/user/user.constants.ts
export const FETCHING_USERS = "FETCHING_USERS";
export const USERS_FETCH_SUCCESSFUL = "USERS_FETCH_SUCCESSFUL";
export const ERROR_FETCHING_USERS = "ERROR_FETCHING_USERS";

Action creators

Action creators are helper functions that create and return actions. Knowing that, let’s create one as follows:

//src/app/user/user.actions.ts
import {
    USERS_FETCH_SUCCESSFUL,
    ERROR_FETCHING_USERS,
    FETCHING_USERS
} from "./user.constants";
export const usersFetchSuccessful = users => ({
    type: USERS_FETCH_SUCCESSFUL,
    payload: users
});
export const fetchError = error => ({
    type: ERROR_FETCHING_USERS,
    payload: error
});

export const fetchingUsers = () => ({ type: FETCHING_USERS });

Here we export usersFetchSuccessful, fetchError, and fetchingUsers, which will be needed in the components to interact with the NgRx store.

The fetchError() action creator will be invoked if an error occurs, the usersFetchSuccessful() action creator will be invoked once the data is returned successfully from an endpoint, and the fetchingUsers() action creator will be invoked once the API request is initiated.

Creating the reducer

Reducers are pure functions that don’t mutate the state. Instead, they produce a new state. A reducer specifies how the application’s state changes in response to the actions triggered.

Let’s create our reducer as follows:

//src/app/user/user.reducers.ts
import {
    USERS_FETCH_SUCCESSFUL,
    ERROR_FETCHING_USERS,
    FETCHING_USERS
} from "./user.constants";
import { User } from "./user.model";
import { ActionReducerMap } from "@ngrx/store/src/models";
const initialState = {
    loading: false,
    list: [],
    error: void 0
};
export interface UserState {
    loading: boolean;
    list: Array<User>;
    error: string;
}
export interface FeatureUsers {
    users: UserState;
}
export const UserReducers: ActionReducerMap<FeatureUsers> = {
    users: UserReducer
};
export function userReducer(state = initialState, action) {
    switch (action.type) {
        case USERS_FETCH_SUCCESSFUL:
            return { ...state, list: action.payload, loading: false };
        case ERROR_FETCHING_USERS:
            return { ...state, error: action.payload, loading: false };
        case FETCHING_USERS:
            return { ...state, loading: true };
        default:
            return state;
    }
}

Each time an action is dispatched from any component that is connected to the store, the reducer receives the action and tests the type property of the action for each of these cases. If the test does not meet any of these cases, it will return the current state.

Creating the effect

Effects allow us to carry out a specified task, then dispatch an action once the task is done.

Knowing that, let’s create our effect that will handle the entire process of sending a request, receiving a request, and also receiving the error response when the request fails:

//src/app/user/user.effect.ts
import { Actions, Effect, ofType } from "@ngrx/effects";
import { HttpClient } from "@angular/common/http";
import { FETCHING_USERS } from "./product.constants";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { delay, map, catchError, switchMap } from "rxjs/operators";
import { usersFetchSuccessful, fetchError } from "./user.actions";
import { Action } from "@ngrx/store";
import { of } from "rxjs/observable/of";
@Injectable()
export class UserEffects {
    @Effect()
    users$: Observable<Action> = this.actions$.pipe(
        ofType(FETCHING_USERS),
        switchMap(action =>
            this.http
                .get("https://jsonplaceholder.typicode.com/users")
                .pipe(
                    delay(3000),
                    map(usersFetchSuccessful),
                    catchError(err => of(fetchError(err)))
                )
         )
    );
    constructor(private actions$: Actions<Action>, private http: HttpClient) {
        console.log("user effects initialized");
    }
}

Here, the @Injectable decorator is used to decorate the Effect class.

The ofType() method allows us to listen to a specific dispatched action, and in our case, FETCHING_USERS while triggering the switchMap() method allows us to convert our current observable to an AJAX service. The delay() method allows us to show the loading indicator for some time. The map() method allows us to dispatch an action if the AJAX response is successful.

Registering the effect

There are two ways of registering effects. We can do so in the root module or in the feature module. The former approach makes the effect accessible globally across the entire application while the latter restricts its accessibility to a specific module. For the sake of code reusability, the latter is preferred.

//src/app/user/user.module.ts
import { NgModule } from "@angular/core";
import { UserComponent } from "./user.component";
import { BrowserModule } from "@angular/platform-browser";
import { UserEffects } from "./user.effect";
import { EffectsModule } from "@ngrx/effects";
import { StoreModule, Action } from "@ngrx/store";
import { UserReducers } from "./user.reducers";
import { HttpClientModule } from "@angular/common/http";

@NgModule({
    imports: [
        BrowserModule,
        StoreModule.forFeature("featureUsers", UserReducers),
        EffectsModule.forFeature([UserEffects]),
        HttpClientModule
    ],
    exports: [UserComponent],
    declarations: [UserComponent],
    providers: []
})
export class UserModule {}

For a feature module, we would use the forFeature() method on the EffectsModule.

Now that we are done with creating and registering the effect, let’s access the effect from our component.

Creating selectors

If you have used Vuex before, you’ll be familiar with getters, which are similar to NgRx selectors. Selectors are used to derive computed information from the store state. We can call getters multiple times in our actions and in our components.

Knowing this, let’s create our selectors:

//src/app/user/user.selector.ts
import { AppState } from "../app-state";
export const getList = (state: AppState) => state.featureUsers.users.list;
export const getError = (state: AppState) =>
    state.featureUsers.users.error;
export const isLoading = (state: AppState) =>
    state.featureUsers.users.loading;

We’ll need a component to display a loading indicator while we’re waiting for the AJAX request to finish. The component will display our data if the request is successful or display an error message if it isn’t.

Creating components

Let’s create a component to display our data, as well as a loading indicator, while our AJAX request is pending:

//src/app/user/user.component.ts
import { Component, OnInit } from "@angular/core";
import { AppState } from "../app-state";
import { Store } from "@ngrx/store";
import { fetchingUsers } from "./user.actions";
import { getList, isLoading, getError } from "./user.selector";
@Component({
    selector: "users",
    template: `
        Users:
        <div *ngFor="let user of users$ | async">
            {{ user.email }}
        </div>
        <div *ngIf="loading$ | async; let loading">
            <div *ngIf="loading">
            fetching users...
            </div>
        </div>
        <div *ngIf="error$ | async; let error" >
            <div *ngIf="error">{{ error }}</div>
        </div>
`
})
export class UserComponent implements OnInit {
    users$;
    loading$;
    error$;
    constructor(private store: Store<AppState>) {
        this.users$ = this.store.select(getList);
        this.loading$ = this.store.select(isLoading);
        this.error$ = this.store.select(getError);
    }
    ngOnInit() {
        this.store.dispatch(fetchingUsers());
    }
}

Here the store is injected through the constructor, so we can access the properties of the state object from the store through the store’s select() method. The store’s select method returns an observable that is rendered in the template using the async pipe.

With this, let’s update AppState:

//src/app/app-state.ts
import { FeatureUsers } from "./user/user.reducer";
export interface AppState {
    featureUsers: FeatureUsers;
}

Because AppState now knows the structure of the resulting user object, our component is able to trigger the store.select() method.

Also, let’s update appModule:

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { EffectsModule } from "@ngrx/effects";
import { AppComponent } from "./app.component";
import { StoreModule } from "@ngrx/store";
import { UserModule } from "./user/user.module";
@NgModule({
    declarations: [AppComponent],
    imports: [
        BrowserModule,
        EffectsModule,
        StoreModule.forRoot({}),
        EffectsModule.forRoot([]),
        UserModule
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule {}

Let’s see what we have been building so far on our browser with the following command:

npm start

Conclusion

In this article, we demonstrated how to handle side effects in our NgRx applications using the @ngrx/effects library while building on some Redux concepts like actions, reducers, and constants. Also, we were able to create effects for handling pending requests, errors in AJAX requests, and successful AJAX requests.

For more information on NgRx, you can check out NgRx’s official documentation here. And here is the GitHub repo for this tutorial.

Reach out in the comment section below if you have any questions or suggestions.

Experience your Angular apps exactly how a user does

Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket. https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Angular apps - .

Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

Leave a Reply