Mastering Asynchronous Data Changes in NgRx Applications
The Rise of Redux and the Need for Efficient State Management
With Redux gaining widespread popularity in the frontend ecosystem, leading frameworks like Angular have adopted it as a reliable state management library. However, Redux architecture lacks built-in functionality for handling asynchronous data changes, also known as side effects, to the Redux state tree. This limitation can significantly impact the performance and reliability of applications.
Understanding Side Effects
Side effects refer to operations that usually finish sometime in the future, such as:
- fetching data from a remote server
- accessing local storage
- recording analytics events
- accessing files
When building an app, it’s essential to account for these asynchronous actions, considering scenarios like API requests, successful data retrieval, and error handling.
Practical Application: Creating an NgRx Effects Library
To handle side effects in NgRx applications, we’ll introduce the @ngrx/effects
library. Let’s create a new Angular project and install the required dependencies.
ng new my-app
cd my-app
ng add @ngrx/effects
We’ll then set up a feature module for users and create a constants file to hold:
FETCHING_USERS
USERS_FETCH_SUCCESSFUL
ERROR_FETCHING_USERS
export const FETCHING_USERS = '[Users] Fetching Users';
export const USERS_FETCH_SUCCESSFUL = '[Users] Users Fetch Successful';
export const ERROR_FETCHING_USERS = '[Users] Error Fetching Users';
Action Creators and Reducers
Action creators are helper functions that create and return actions. We’ll create one to interact with the NgRx store.
import { createAction } from '@ngrx/store';
export const fetchUsers = createAction(
FETCHING_USERS,
props<{ payload: { userId: number } }>()
);
Reducers, on the other hand, are pure functions that produce a new state. Let’s create our reducer to handle the application’s state changes in response to actions.
import { createReducer } from '@ngrx/store';
const initialState: any = [];
const usersReducer = createReducer(
initialState,
{
[FETCHING_USERS]: (state) => ({...state, loading: true }),
[USERS_FETCH_SUCCESSFUL]: (state, { payload }) => ({...state, data: payload, loading: false }),
[ERROR_FETCHING_USERS]: (state, { payload }) => ({...state, error: payload, loading: false }),
}
);
export function reducer(state: any = initialState, action: any) {
return usersReducer(state, action);
}
Creating the Effect
Effects allow us to carry out a specified task and dispatch an action once the task is done. We’ll create an effect to handle the entire process of sending a request, receiving a response, and handling errors.
import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { catchError, map, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';
@Injectable()
export class UsersEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(fetchUsers),
switchMap((action) =>
this.userService.getUsers(action.payload.userId).pipe(
map((users) => ({ type: USERS_FETCH_SUCCESSFUL, payload: users })),
catchError((error) => of({ type: ERROR_FETCHING_USERS, payload: error }))
)
)
)
);
constructor(private actions$: Actions, private userService: UserService) {}
}
Registering the Effect and Creating Selectors
We can register effects in the root module or feature module. For code reusability, we’ll use the latter approach.
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { UsersEffects } from './users.effects';
@NgModule({
imports: [
EffectsModule.forRoot([UsersEffects]),
],
})
export class UsersModule {}
Selectors are used to derive computed information from the store state. Let’s create our selectors to access the effect from our component.
import { createSelector } from '@ngrx/store';
export const selectUsers = createSelector(
(state: any) => state.users,
(users) => users.data
);
export const selectLoading = createSelector(
(state: any) => state.users,
(users) => users.loading
);
export const selectError = createSelector(
(state: any) => state.users,
(users) => users.error
);
Creating Components and Displaying Data
We’ll create a component to display our data, along with a loading indicator, while the AJAX request is pending. The component will display an error message if the request fails.
<div>
<ng-container *ngIf="loading">
<span>Loading...</span>
</ng-container>
<ng-container *ngIf="error">
<span>Error: {{ error }}</span>
</ng-container>
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
</div>
import { Component } from '@angular/core';
import { select, selectSnapshot } from '@ngrx/store';
import { selectUsers, selectLoading, selectError } from './users.selectors';
@Component({
selector: 'app-users',
template: '',
})
export class UsersComponent {
users$ = this.store.select(selectUsers);
loading$ = this.store.select(selectLoading);
error$ = this.store.select(selectError);
constructor(private store: Store) {}
}
Putting it All Together
With our effect, reducer, and component in place, let’s update our AppState
and appModule
. Now, let’s see what we’ve built so far on our browser.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { UsersModule } from './users/users.module';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
UsersModule,
StoreModule.forRoot({ users: usersReducer }),
EffectsModule.forRoot([UsersEffects]),
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}