Skip to content
Angular Essentials
GitHub

NgRx effects

Effects are side effect that occurs in an application like API calls, time-based events and so on. Effects are powered by RxJS.

In a service-based Angular application, components are responsible for interacting with external resources directly through services. Instead, effects provide a way to interact with those services and isolate them from the components.

All Actions that are dispatched within an application state are always first processed by the Reducers before being handled by the Effects of the application state.

Effects are long-running services that listen to an observable of every action dispatched from the Store.

Effects perform certain operation based on the type of action and returns a new action.

Installation

ng add @ngrx/effects@latest

Mocking Server

  • Mock server with the following JSON data.

{% content-ref url=“../angular/http-communicating-with-server/mocking-http-server.md” %} mocking-http-server.md {% endcontent-ref %}

{
  "todos": [
    {
      "id": 1,
      "description": "Buy milk",
      "done": true
    },
    {
      "id": 2,
      "description": "Learn RxJS",
      "done": false
    },
    {
      "id": 3,
      "description": "Learn Angular",
      "done": true
    },
    {
      "id": 4,
      "description": "Learn NgRx",
      "done": false
    },
    {
      "id": 5,
      "description": "Learn Angular animation",
      "done": true
    }
  ]
}
  • Run your JSON server

Adjusting Todo Actions and Reducer

Add new action in todo.actions.ts which we will use to fetch todos from server.

export const setTodos = createAction(
  '[Todo Component] Set Todos',
  props<{todos: Todo[]}>()
);

Also, adjust reducer that sets the todos in the state.

export const todoReducer = createReducer(
  todoInitialState,
  on(TodoActions.setTodos, (state, payload) => {
    const todos = payload.todos;
    return {...state, todos};
  }),
  on(TodoActions.saveOrUpdateTodo, (state, payload) => {
    const todos = payload.isUpdate
      ? state.todos.map(todo => todo.id === payload.todo.id ? {...todo, done: !todo.done} : todo)
      : [...state.todos, payload.todo];
    return {...state, todos};
  }),
  on(TodoActions.deleteTodo, (state, payload) => {
    const todo = state.todos.find(todo => todo.id === payload.todoId);
    if (todo) {
      const stateCopy = _.cloneDeep(state);
      const index = stateCopy.todos.findIndex(todo => todo.id === payload.todoId);
      stateCopy.todos.splice(index, 1)
      return {...state, todos: stateCopy.todos}
    }
    return state;
  })
);

Now, the question remains when do we want fetch the todos. One good place would be when our TodoComponent initializes. Go ahead and dispatch fetchTodos() in ngOnInit().

export class TodoComponent implements OnInit {

  ngOnInit(): void {
    this.store.dispatch(fetchTodos());
  }
}

Writing Todo Effects

Create a file viz todo.effects.ts inside store directory.

import {Injectable} from '@angular/core';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import * as TodoActions from './todo.actions';
import {switchMap} from 'rxjs/operators';
import {HttpClient} from '@angular/common/http';
import {Todo} from '../todo.model';

@Injectable()
export class TodoEffects {

  constructor(private readonly actions$: Actions,
              private readonly http: HttpClient) {
  }

  fetchTodos$ = createEffect(() => this.actions$.pipe(
    ofType(TodoActions.fetchTodos),
    switchMap(() => {
      return this.http.get<Todo[]>(`http://localhost:3000/todos`).pipe(
        switchMap(response => [TodoActions.setTodos({todos: response})]),
        catchError((err: HttpErrorResponse | TimeoutError) => EMPTY)
      );
    })
  ));
}

Okay, there is a lot going on above Effects. Let’s break down each line.

Effects are decorated with @Injectable meaning they are injectable service classes.

private readonly actions$: Actions creates an injectable Actions service that provides an observable stream of all actions dispatched after the latest state has been reduced.

Metadata is attached to the observable streams using the createEffect function. The metadata is used to register the streams that are subscribed to the store. Any action returned from the effect stream is then dispatched back to the Store.

this.actions$.pipe() allows us to use multiple pipeable operators so that we can transform data streams as we require.

To determine when fetchTodos$ effect is executed we use ofType operator. ofType(TodoActions.fetchTodos) means fetchTodos$ effect is executed when TodoActions.fetchTodos is dispatched. The ofType operator takes one or more action types as arguments to filter on which actions to act upon.

The stream of actions is then flattened and mapped into a new observable using the switchMap operator.

Since http.get() returns an observable we again use pipe to transform the response we get from server. switchMap() is basically used to cancel previous http requests when a new one arrives.

At last [TodoActions.setTodos({todos: response})] is returned meaning after we get response from http server, we want effect to dispatch the setTodos(). Notice we returned array meaning effect can dispatch multiple actions. Also, we didn’t manually write store.dispatch, effect does this for you.

catchError() determines what to do if any error occurs. Currently, it returns an empty observable if an error occurs. (err: HttpErrorResponse | TimeoutError) is just to give you an example of what kind of errors you would be catching in http request.

In a similar way, you can write effect to handle adding and deleting todo from server.

The fetchTodos$ effect is listening for all dispatched actions through the Actions stream, but is only interested in the TodoActions.fetchTodos event using the ofType operator.

Note: Effects are subscribed to the Store observable.

Registering Todo Effects

import { EffectsModule } from '@ngrx/effects';

@NgModule({
  imports: [
    EffectsModule.forRoot(),
  ],
})
export class AppModule {
}

Note: The EffectsModule.forRoot() method must be added to your AppModule imports even if you don’t register any root-level effects.

Effects start running immediately after the AppModule is loaded to ensure they are listening for all relevant actions as soon as possible.

For feature modules, register your effects by adding the EffectsModule.forFeature() method in the imports array of your NgModule.

import { EffectsModule } from '@ngrx/effects';

@NgModule({
  imports: [
    EffectsModule.forFeature([TodoEffects]),
  ]
})

Note: Running an effects class multiple times, either by forRoot() or forFeature(), (for example via different lazy loaded modules) will not cause Effects to run multiple times. There is no functional difference between effects loaded by forRoot() and forFeature(); the important difference between the functions is that forRoot() sets up the providers required for effects.

An alternative way of registering effects

You can provide root/feature-level effects with the provider USER_PROVIDED_EFFECTS.

providers: [
  MovieEffects,
  {
    provide: USER_PROVIDED_EFFECTS,
    multi: true,
    useValue: [MovieEffects],
  },
]

The EffectsModule.forFeature() method must be added to the module imports even if you only provide effects over token, and don’t pass them via parameters. (Same goes for EffectsModule.forRoot()). {{< /step >}}

What’s next? Other recipes NgRx offers like

  • Meta-Reducers
  • Feature Creators
  • Injecting Reducers
  • Runtime checks
  • NgRx Router State
  • NgRx Entity
  • NgRx Component Store