Themes

Select a theme

Ngrx Effects - Isolate side-effects in angular applications

 

Table of contents
  • What is a Side effect?

  • Why NgRx Effects?

    • Service based design vs NgRx Effects based design
  • NgRx Effects

    • Installation

    • Implementation

    • Register NgRx Effects in module

 

what is a Side-effect?

A side effect refers simply to the modification of some kind of state - for instance:

  • Changing the value of a variable;

  • Writing some data to disk;

  • Enabling or disabling a button in the User Interface.

source

 

Why NgRx Effects?

A pure component has an immutable input and produces the events as output. These components are essentially dumb and most of the computations are done outside of it. A pure component has a single responsibility similar to pure functions.

Now, If the components are not doing any computations, most of the computations and side-effects would be moved to the services. In fact, that is the first-hand use of angular services. It is easy to inject them into components and perform various computations.

Now here are a few things you should consider -

  • First of all, if your application is growing fast and you are storing/managing state inside your components or services, you should consider a state management solution.

  • The services are an ideal choice but you have to manually take care of storing the state, making sure the state remains immutable, consistent, and available for all the dependent components as well as services.

  • Let’s assume, you have a service taking care of communicating via APIs to some remote server. You inject the service in a component and call the method of the service inside the component.

  • …and if you have another service that performs some computation which is scheduled to be run when the user interacts with UI such as a click of a button. Again, you would inject the service in component, and on click of the button, a method of the service is called.

It works great… But if you notice, the component is tighly coupled with the service and it knows about what operations to perform when a user clicks a button or when an API should be called.

The very first disadvantage of this pattern you would come across is components are hard to test since they are dependent on many services. You would also notice that It is almost impossible to reuse the components.

 

So what is the alternative approach?

The alternative approach is to let components be pure and not responsible for managing the state . Instead make them reactive so that when an input data is available, it renders it to UI and when there are UI interactions, it generates the relevant Events.

That is it…

The component does not have to know, how the Input data is made available or how the events are handled.

The services are nevertheless an important part of the application. They would still contain all the methods to do various computations or communicate via APIs. But now they are no longer being injected inside the component.

I have already written a post about reactive state management using NgRx store, actions, and selectors. You can go through it to have an understanding of NgRx state management.

We discussed the service based approach above. Let’s see a comparison between the service based design and NgRx effects.

You might notice fewer elements in play during service based design but don’t let it fool you. It is better to have more elements in application than to have a lousy app.

 

 

Service based design

Suppose, our AppComponent requires a list of users.

  • We have a service AppRemoteService and it contains users$ observable which can be subscribed to get a list of users.

    users$ = this.httpClient.get<User[]>(URL).pipe(take(1));
    
  • We have injected the AppRemoteService in side the AppComponent and we would subscribe to AppRemoteService.users$ observable .

    @Component({
        template: `
        <div class="user-container" 
          *ngIf="localUsers">
         <app-user *ngfor="let user of localUsers" 
                    [inputUser]="user">
      </div>
        `
    })
    export class AppComponent{
    //state inside component
    localUsers: User[];
      
    constructor(private remoteService: RemoteService){}
      
      ngOnInit(){
          //handle the subscription here
      this.remoteService.users$.subscrible(
          users => this.localUsers = users;
          );
      }
      } 
    

NgRx Effects based design

Here is how NgRx effects will change it -

  • The AppComponent would only require NgRx Store to select the state or dispatch actions.

    export class AppComponent implements OnInit {
     constructor(private store: Store<fromApp.AppState>) { }
     }
    
  • As soon as the component requires the list of users, it would dispatch an action loadUsers when the component is initialized.

    export class AppComponent implements OnInit {
     constructor(private store: Store<fromApp.AppState>) { }
      
     ngOnInit(): void {
         //action dispatched
        this.store.dispatch(fromActions.loadUsers());
     }
      
    }
    
  • The Component will use NgRx selector selectUsers and subscribe to its observable.

      
    @Component({
     template: `
         <div class="user-container" 
             *ngIf="localUsers$ | async as users">
             <app-user *ngfor="let user of users" 
                        [inputUser]="user">
         </div>
     `
    })
    export class AppComponent implements OnInit {
      localusers$ = this.store.select(fromSelectors.selectUsers);
      
      constructor(private store: Store<fromApp.AppState>) { }
      
      ngOnInit(): void {
        this.store.dispatch(fromActions.loadUsers());
      }
    }
    
  • NgRx Effects will be listening to the stream of actions dispatched since the latest state change. loadUsers$ effect is interested in loadUsers action dispatched by AppComponent. As such, when the component is initialized, the loadUsers action is dispatched. The effect reacts to it and subscribes remoteservice.users$ .

  • Once the data is fetched, the loadUsers$ effect will dispatch addUsers action with associated metadata - users. The respective reducer function will transition the state. The latest state will contain recently feteched users.

    //app.effects.ts
     loadUsers$ = createEffect(
        () => this.action$.pipe(
            ofType(AppActions.loadUsers),
            mergeMap(() => this.remoteService.users$
            .pipe(
                map(users => AppActions.addUsers({ users })),
                catchError(error => {
                return of(error);
                })
            )),
     ));
    
      
    //app.reducer.ts
    //addUsers action mapping 
      
    const theReducer = createReducer(
      initialState,
      on(AppActions.addUsers, (state, { users }) => ({
        ...state,
        users: [...users]
      }))
      
    );
    
  • As soon as the data is available, the localusers$ observable subsrciption will have users list ready for the component to render.

In contrast with the service-based approach isolating the side-effects using NgRx Effects, the component is not concerned about how the data is loaded. Besides allowing a component to be pure, It also makes testing components easier and increases the chances of reusability.

 

NgRx Effects

NgRx Effects are injectable services similar to Angular services. These services are long running and listen to an observable stream of all Actions dispatched. If the effect is interested in any action , it performs a task(side-effects) and return another Action back to Action Stream.

NgRx Effects may not always dispatch a new action upon completion of a side-effect.

Installation

# angular CLI 6+
ng add @ngrx/effects@latest
# < 6
npm install @ngrx/effects --save

Here is the complete project setup.

Below is the service which contains methods for fetching data via API’s.

//app-remote.service.ts

const USERS_PATH = 'https://jsonplaceholder.typicode.com/users';
const POSTS_PATH = 'https://jsonplaceholder.typicode.com/posts';

@Injectable({
  providedIn: 'root'
})
export class AppRemoteService {
  constructor(private httpClient: HttpClient) { }

  /** 1) - fetch users from the remote server */
  users$ = this.httpClient.get<User[]>(USERS_PATH).pipe(take(1));

  /** 2) - fetch posts from the remote server */
  posts$ = this.httpClient.get<Post[]>(POSTS_PATH).pipe(take(1));

}

 

Implementation

Flow diagram of NgRx Effects

  • We would require an injectable service for create effects. Let’s go ahead and create an Effect service, name it AppEffects and make it injectable using @Injectable decorator.

  • NgRx Effects listens to an Observable Action stream dispatched since the latest state change and it provided by Actions service. Let’s inject an instance of Actions service in our AppEffects service.

    import { Actions } from '@ngrx/effects/';
      
    @Injectable()
      
      export class AppEffects{
          constructor( private action$: Actions){}
     }
    
  • The sole purpose of NgRx Effects is to isolate the side-effects from components. Instead of directly calling side-effects inside components, we would now call them in NgRx effects. As such, other services can also be injected into AppEffect service. These services can be used to interact with external apis or perfrom computations. We will inject our AppRemoteService which contains methods to fetch data via APIs.

    import { Actions } from '@ngrx/effects/';
    import { AppRemoteService } from '../app-remote.service';
      
    @Injectable()
     export class AppEffects{
      
       constructor( 
           private action$: Actions,
           private remoteService: AppRemoteService
         ) { }  
    }
    

Inside the AppEffects service, The createEffect function is used to create the effects. It takes two arguments. Lets see how each of these arguments are defined.

The first argument is a function which returns an observable.

() => Observable<Action | unknown>

Below are the steps, we will follow to create this function–

  • First of all, we will access the Actions instance observable.

  • …then we will use pipeable ofType operator function. ofType operator function takes one or more actions as arguments and filters the Actions stream based on provided arguments.

    () => this.actions$.pipe(
        ofType(AppActions.loadUsers)
    )
    
  • The stream of actions is flattened and mapped to new observables using a RxJs flattening operators such as MergeMap, concatMap, exhaustMap. At this point, the computation or external API calls are performed depending on the task.

    () => this.actions$.pipe(
         ofType(AppActions.loadUsers),
         //flatten the actions
         mergeMap(
         (action) => this.remoteService.users$
    )
    

    We have provide only one action as argument to ofType operator function. But you could provide multiple actions. In that case, the effect will execute the tasks for any matched Action.

  • Finally

    • If the task is successful, a new action with optional metadata is returned as an observable

    • If an error occurs, an Observable of error is returned.

    () => this.actions$.pipe(
        ofType(AppActions.loadUsers),
        //flatten the actions
        mergeMap((action) => this.remoteService.users$
            .pipe(
                //maps the users to addUser action
                map(users => AppActions.addUsers({ users })),
                // return Observable<any> to catch error
                catchError(error => {
                    return of(error);
                 })
             )
        );
    

The second argument is the EffectConfig to configure the effect

interface EffectConfig{
   dispatch: boolean;
   useEffectsErrorHandler: boolean;
}
  • Dispatch:

    • dipatch:true conveyes that the  action emitted by the effect is dispatched to the store.

    • dipatch:false means the effect does not need to return an action.

  • useEffectErrorHandler determines if the effect should be resubscribed incase an error occurs.

Below is the final version loadUsers$ effect -

 loadUsers$ = createEffect(
 () => this.action$.pipe(
    ofType(AppActions.loadUsers),
    mergeMap(() => this.remoteService.users$
      .pipe(
        map(users => AppActions.addUsers({ users })),
        catchError(error => {
          return of(error);
        })
      )),
  ));

Similarly we can create loadPosts$ effect. which is mapped to loadPosts action and return addPosts action

 

 

Effects that require input state.

Let’s assume, our app also requires to show notifications upon operation success.

  • We have a showNofitication(message:string) method which is responsible to show the notifications. The messages shown by the notification is contextual and hence the message should be passed to the method as argument .

  • We also have an action [Notification] operation Success which carries the message metadata.

  • The effect will access the metadata of the selected action and pass it on to the showNotification Method

//app.actions.ts

export const operationSuccess = createAction(
 '[profile] operation success',
 props<{message: message}>()
);
//app.effects.ts

  operatorSuccessNotify$ = createEffect(() => this.action$.pipe(
    ofType(AppActions.operationSuccess),
    mergeMap((action) => 
       of(
           this.notificationService.successNotification(action.message))
       )
  ),
    {
      dispatch: false
    }
  );
);

Notice: dispatch is set to false for this effect. That means, it does not return any new action.

There could also be scenarios where the lastest state of the application is required in the NgRx Effects. In that case, the state has to be accessed directly using NgRx Selectors and RxJSwithLatestFromoperator function. withLatestFrom operator function should be used inside a flatening operator to prevent the selector from firing untill the correct action is dispatched.

Lets say, the effect requires latest number of posts, we will use selectPosts selector.

    () => 
        this.action$.pipe(
            ofType(AppActions.updateSize),
            concatMap((action) => 
                    of(action).pipe(
                    withLatestFrom(
                            this.store.select(fromSelectors.selectPosts)
                     )) 
             ),

             ....
        )
)

 

Register the effects

Depending on the requirement, you can either create one Effects service for all application or seperate Effect services for each feature module. That means we can be register Effects in the application module or seperately in feature modules.

EffectsModule.forRoot([AppEffects]);
//OR
EffectsModule.forFeature([ProfileFeatureEffects]);