NgRx

Featured image

NgRx - Reactive State for Angular

Howdyyy? In this post I want to share something that I had left in the near past, about the concepts, experiences, learnings, advantages/disadvantages of implementing reactive states in Angular with NgRx

What is it?

In short, it is a state handler using rxjs for Angular, applying the data architecture pattern -> Redux and rxjs. enter image description here

What does this mean and how does it work?

enter image description here

enter image description here

Let’s take it one step at a time:

Store

The Store for us will be the single source of truth. And it reflects the current condition of our app.

Actions

Actions are one-time events that occur in our app, user interactions, network requests, etc.

Reducers

Reducers are pure functions, responsible for managing transitions, i.e. they are the ones who will react to the dispatched actions, taking the current state and the last one to generate a new state with this change (without mutating).

Selectors

Selectors are pure functions to retrieve portions of the state from the Store. Thanks to them, our app can listen to the changes in the states.

Effects

The Effects manage the secondary effects of each action. That is, to communicate with external services and apis when a specific action is dispatched, or also to dispatch another action that updates another part of the State.

Functional sample

So we can say that from a button of our component, we need to perform an action, for example, save. For this case, our component is only going to dispatch one action (save). This action could have a secondary effect (save that data in an api). So, our reducer will react to that action by saving that data in the Store and executing that side effect (calling the api with the data to save). In the case that this or any other component needs to know some portion of that data, it will make use of the selectors that we have assembled for that purpose.

In this way, we can clearly see that the component can only dispatch an action or make use of a selector. Nothing else. All the rest of the flow (unidirectional) does not pass through the component, it is totally isolated from this logic. The component can never modify, in any way, the Store, and it is for this reason that we are applying the Redux data architecture.

Real coding sample

Having previously installed our lib “@ngrx/store”: “x.x.x”. Let’s suppose a list of movies, where we can add, delete and toggle (active/inactive).

We generate the possible actions in a movie.actions.ts:

import {createAction, props} from  '@ngrx/store';

export  const  Add = createAction('[MovieComponent] Add', props<{text: string}>());
export  const  Remove = createAction('[MovieComponent] Remove', props<{id: string}>());
export  const  Toggle = createAction('[MovieComponent] Toggle', props<{id: string}>());

We create the reducers, the pure functions that will react to the actions we have just set up at movie.reducers.ts:

import { createReducer, on } from  '@ngrx/store';
import { Movie } from  './interfaces'; // interface a tu gusto
import { Add, Remove, Toggle } from  './actions';
import  *  as  uuid  from  'uuid';

const initialState: Array<Movie> = [];
export const movieReducer = createReducer(initialState,
on(Add, (state, action) => ([...state, { id:  uuid.v4(), text:  action.text, movie:  true }])),
on(Remove, (state, action) =>  state.filter(i  =>  i.id !== action.id)),
on(Toggle, (state, action) =>  state.map(i  =>  i.id === action.id ? {...i, movie: !i.movie} : i)),
)

Setup our app.module:

import { StoreModule } from  '@ngrx/store';
import { movieReducer } from  './movies/reducer';
   ...
   StoreModule.forRoot({
   	    movies:  movieReducer

})

And now, let’s see how from our component we can retrieve those movies (selector) and how we dispatch the actions at movie.component.ts

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Movie } from './movies/interfaces';
import { Add, Remove, Toggle } from './movies/actions';
import { Observable } from 'rxjs';

@Component({
  selector: 'movie-app',
  templateUrl: './movie.component.html',
  styleUrls: ['./movie.component.css']
})

export class MovieComponent {
  newMovieText: string = "";
  movies$: Observable<Movie[]> = this.store.select(state => state.movies);

  constructor(private store: Store<{ movies: Movie[] }>) {}

  addMovie() {
    this.store.dispatch(Add({ text: this.newMovieText || 'Untitled movie' }));
    this.newMovieText = '';
  }

  removeMovie(id) {
    this.store.dispatch(Remove({ id }));
  }

  toggleMovie(id) {
    this.store.dispatch(Toggle({ id }));
  }
}

In this example we don’t add side effects… but if we get a little spicy we could be the first puppy. enter image description here

For this, we could generate a moviesService to retrieve movies from some random api (as needed):

@Injectable({
  providedIn: 'root'
})
export class MoviesService {
  constructor (private http: HttpClient) {}

  getAll() {
    return this.http.get('/movies');
  }
}

Register effect at app.module.ts:

EffectsModule.forRoot([MovieEffects])

and now, our movie.effects.ts to get them:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { EMPTY } from 'rxjs';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { MoviesService } from './movies.service';

@Injectable()
export class MovieEffects {

  loadMovies$ = createEffect(() => this.actions$.pipe(
    ofType('[Movies Page] Load Movies'),
    mergeMap(() => this.moviesService.getAll()
      .pipe(
        map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),
        catchError(() => EMPTY)
      ))
    )
  );

  constructor(
    private actions$: Actions,
    private moviesService: MoviesService
  ) {}
}

This way, our component only dispatches the load:

this.store.dispatch({ type:  '[Movies Page] Load Movies'  });
//or we create the corresponding action and dispatch:
//this.store.dispatch(loadMovies());

Bonus!

enter image description here Chrome extension you will need for debugging: Redux DevTools

Conclusion

Making a comparison between using or not using this pattern, what happens when we do not use it and we do it in a conventional way, subscribing to a service, and delegating the rest of the tasks of what the component in question receives, it is clear that the component has multiple responsibilities such as:

This is why we can see that effects, decrease the responsibility of our components. In a larger application, this becomes more important because our components start to grow in quantity, we have many data sources, with many services required to get those pieces of data, and services that even depend on other services.

Therefore and following the previous exercise, the component is no longer in charge of how the movies are obtained and loaded. It is only responsible for declaring its intention to load movies and to use selectors to access the movie list data. This allows the component to be easier to test and less responsible for the data it needs.

It is worth noting, that the effect we generated in the loadMovies$ exercise, is listening for all actions sent through the Actions stream, but is only interested in the [Movies Page] Load Movies event, that is why we use the ofType operator. The Actions stream is then flattened and mapped into a new observable using the mergeMap operator. The MoviesService#getAll() method returns an observable that maps the movies to a new action on success, and currently returns an empty observable if an error occurs. The action is sent to the [Store] where it can be handled by the reducers when a change of state is needed. If we would also like to add error handling to this case, we can see it here!

In my opinion, if you are developing a medium/large and complex app, it is worth (even the boilerplate at the beginning), to invest and dedicate time and effort to NgRx, because in a short/medium term, you start to notice its fruits, facilitating the debugging, scalability and maintenance of the app, as well as the understanding, transparency and scalability to work even with new developers entering the teams.

Happy coding!