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 {}

Leave a Reply