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.
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:
FETCHING_USERS
USERS_FETCH_SUCCESSFUL
ERROR_FETCHING_USERS
Let’s get our hands dirty by doing some practice.
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:
//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 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.
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.
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.
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.
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.
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
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.
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.
LogRocket is like a DVR for web and mobile 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 — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to balance vibrant visuals with accessible, user-centered options like media queries, syntax, and minimized data use.
Learn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
2 Replies to "Handling side effects in an Angular + Redux application"
Typo in the reducer code: `users: UserReducer` should be corrected `users: userReducer`
Thanks for the catch, noted and updated