Angular NGRX with Star wars API

ilyoskhuja

Ilyoskhuja

Posted on April 2, 2022

Angular NGRX with Star wars API

In this article we will create angular application with ngrx and Star Wars API.To understand what is ngrx and how we can you it with angular lets jump to ngrx documentation.

What is NgRx?

NgRx is a framework for building reactive applications in Angular. NgRx provides libraries for:

  • Managing global and local state.
  • Isolation of side effects to promote a cleaner component architecture.
  • Entity collection management.
  • Integration with the Angular Router.
  • Developer tooling that enhances developer experience when building many different types of applications.

Image description

Store

NgRx Store provides state management for creating maintainable, explicit applications through the use of single state and actions in order to express state changes. In cases where you don't need a global, application-wide solution to manage state, consider using NgRx ComponentStore which provides a solution for local state management.

Effect

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. Effects are where you handle tasks such as fetching data, long-running tasks that produce multiple events, and other external interactions where your components don't need explicit knowledge of these interactions.

Reducer

Generates a reducer file that contains a state interface, an initial state object for the reducer, and a reducer function.

Actions

Actions are one of the main building blocks in NgRx. Actions express unique events that happen throughout your application. From user interaction with the page, external interaction through network requests, and direct interaction with device APIs, these and more events are described with actions.

Lets create our app, first you should create angular app you can find "get started" link here.For backend we will use SWAPI

Lets create reducer for our application, first we will create folder "reducers" and inside we need create index.ts file.

import {
  ActionReducerMap,
  createFeatureSelector,
  createSelector,
  MetaReducer
} from '@ngrx/store';
import { environment } from '../../environments/environment';
import * as fromMovies from '../movies/movies.reducer';

export interface State {
  movies: fromMovies.State;
}

export const reducers: ActionReducerMap<State> = {
  movies: fromMovies.reducer,
};


export const metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];

export const getMoviesState = createFeatureSelector<fromMovies.State>('movies');
export const getMovies = createSelector(getMoviesState, state => state.data);
export const getIsLoading = createSelector(getMoviesState, state => state.isLoading);
export const getMovieCharacters = createSelector(getMoviesState, state => state.selectedMovieCharacters);
export const getMovie = createSelector(getMoviesState, state => state.selectedMovie);
export const getCharacterMovies = createSelector(getMoviesState, state => state.selectedCharacterMovies);
export const getCharacter = createSelector(getMoviesState, state => state.selectedCharacter);

// export const getCurrentPage = createSelector(getMoviesState, state => state.page);
// export const getIsFirstPage = createSelector(getMoviesState, state => !state.previous);
// export const getIsLastPage = createSelector(getMoviesState, state => !state.next);

Enter fullscreen mode Exit fullscreen mode

and we will create characters and movies components inside app( you can find code from github),but in this article I want to show ngrx part.Next step is creating movies.effects.ts

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
// import { getCurrentPage } from '../reducers/index';
import { State } from './movies.reducer';
import { MovieService } from './movie.service';
import {
  MoviesActionTypes,
  MoviesActions,
  FetchMovies,
  FetchMoviesSuccess,
  FetchMoviesError,
  FetchMovieCharactersSuccess,
  FetchMovieCharactersError,
  FetchMovieError,
  FetchMovieSuccess,
  FetchCharacterError,
  FetchCharacterSuccess,
  FetchCharacterMoviesSuccess,
  FetchCharacterMoviesError
} from './movies.actions';
import { Observable, of } from 'rxjs';
import { map, switchMap, catchError, withLatestFrom } from 'rxjs/operators';
import { CharactersService } from '../characters/characters.service';
import { Movie } from './models/movie';

@Injectable()
export class MoviesEffects {

  fetch$ = createEffect(() => {
    return this.actions$
      .pipe(
        ofType(MoviesActionTypes.FetchMovies),
        withLatestFrom(this.store),
        switchMap(([action, state]) =>
          this.service.getMovies().pipe(
            map(data => new FetchMoviesSuccess(data)),
            catchError(err => of(new FetchMoviesError(err)))
          )
        )
      )
  });
  fetchCharacters$ = createEffect(() => {
    return this.actions$
      .pipe(
        ofType(MoviesActionTypes.FetchMovieCharacters),
        withLatestFrom(this.store),
        switchMap(([action, state]) =>

          this.charactersService.getCharactersByFilm(this.service.selectedFilm).pipe(
            catchError(err => of(new FetchMovieCharactersError(err))),
            map(data =>

              new FetchMovieCharactersSuccess(data)
              // (characters: Movie['charactersData']) => {
              // console.log("characters:", characters);
              // // this.movieService.selectedFilm.charactersData=[];
              // console.log("this.movieService.selectedFilm.charactersData:", this.movieService.selectedFilm.charactersData);


              // //  this.movieService.selectedFilm.charactersData = characters;
              // return true;
              // }
            )
          )
        ))
  });

  fetchCharacterMovies$ =  createEffect(() => {
    return this.actions$
      .pipe(
        ofType(MoviesActionTypes.FetchCharacterMovies),
        withLatestFrom(this.store),
        switchMap(([action, state]) =>

          this.service.getFilmsByCharacter(this.service.selectedCharacter).pipe(
            catchError(err => of(new FetchCharacterMoviesError(err))),
            map(data =>

              new FetchCharacterMoviesSuccess(data)
              // (characters: Movie['charactersData']) => {
              // console.log("characters:", characters);
              // // this.movieService.selectedFilm.charactersData=[];
              // console.log("this.movieService.selectedFilm.charactersData:", this.movieService.selectedFilm.charactersData);


              // //  this.movieService.selectedFilm.charactersData = characters;
              // return true;
              // }
            )
          )
        ))
  });

  fetchMovie$ = createEffect(() => {
    return this.actions$
      .pipe(
        ofType(MoviesActionTypes.FetchMovie),
        withLatestFrom(this.store),
        switchMap(([action, state]) =>
          this.service.getFilm(this.service.selectedFilm.id).pipe(
            catchError(err => of(new FetchMovieError(err))),
            map(data =>

              new FetchMovieSuccess(data)
              // (characters: Movie['charactersData']) => {
              // console.log("characters:", characters);
              // // this.movieService.selectedFilm.charactersData=[];
              // console.log("this.movieService.selectedFilm.charactersData:", this.movieService.selectedFilm.charactersData);


              // //  this.movieService.selectedFilm.charactersData = characters;
              // return true;
              // }
            )
          )
        ))
  });
  // this.service.getMovies().pipe(
  //   map(data =>
  //     new FetchMovieCharactersSuccess(data)
  //   ),
  //   catchError(err => of(new FetchMovieCharactersError(err)))
  // )
  //   )
  // );
  fetchCharacter$ = createEffect(() => {
    return this.actions$
      .pipe(
        ofType(MoviesActionTypes.FetchCharacter),
        withLatestFrom(this.store),
        switchMap(([action, state]) =>
          this.charactersService.getCharacter(this.service.selectedCharacter.id).pipe(
            catchError(err => of(new FetchCharacterError(err))),
            map(data =>

              new FetchCharacterSuccess(data)
            )
          )
        ))
  });
  paginate$ = createEffect(() => {
    return this.actions$
      .pipe(
        ofType(MoviesActionTypes.ChangePage),
        map(() => new FetchMovies())
      )
  });

  constructor(private actions$: Actions,
    private store: Store<State>,
    private service: MovieService,
    private charactersService: CharactersService) { }
}

Enter fullscreen mode Exit fullscreen mode

we will use createEffect function (Creates an effect from an Observable and an EffectConfig).

For movie.reducer.ts we will use code below

import { Action } from '@ngrx/store';
import { MoviesActions, MoviesActionTypes, Pagination } from './movies.actions';
import { Movie } from './models/movie';
import { HttpErrorResponse } from '@angular/common/http';
import { Character } from '../characters/models/character';

export interface State {
  isLoading: boolean;
  error: HttpErrorResponse | null;
  data: Movie[] | null;
  selectedMovieCharacters: [] | null;
  selectedMovie:Movie| null;
  selectedCharacterMovies: [] | null;
  selectedCharacter: Character | null;

  // next: string | null;
  // previous: string | null;

}

export const initialState: State = {
  isLoading: false,
  error: null,
  data: [],
  selectedMovieCharacters:[],
  selectedMovie: null,

  selectedCharacter: null,
  selectedCharacterMovies:[]


  // next: null,
  // previous: null,

};

export function reducer(state = initialState, action: MoviesActions): State {
  switch (action.type) {

    case MoviesActionTypes.FetchMovies:
      return {
        ...state,
        isLoading: true,
        error: null
      };

    case MoviesActionTypes.FetchMoviesSuccess:
      return {
        ...state,
        isLoading: false,
        data: action.payload,
        // next: action.payload.next,
        // previous: action.payload.previous
      };

    case MoviesActionTypes.FetchMoviesError:
      return {
        ...state,
        isLoading: false,
        error: action.payload
      };
      case MoviesActionTypes.FetchMovie:
        return {
          ...state,
          isLoading: true,
          error: null
        };

      case MoviesActionTypes.FetchMovieSuccess:
        return {
          ...state,
          isLoading: false,
          selectedMovie: action.payload,
          // next: action.payload.next,
          // previous: action.payload.previous
        };

      case MoviesActionTypes.FetchMovieError:
        return {
          ...state,
          isLoading: false,
          error: action.payload
        };

    case MoviesActionTypes.FetchCharacter:
      return {
        ...state,
        isLoading: true,
        error: null
      };

    case MoviesActionTypes.FetchCharacterSuccess:
      return {
        ...state,
        isLoading: false,
        selectedCharacter: action.payload,
        // next: action.payload.next,
        // previous: action.payload.previous
      };

    case MoviesActionTypes.FetchCharacterError:
      return {
        ...state,
        isLoading: false,
        error: action.payload
      };

    case MoviesActionTypes.FetchCharacterMovies:
      return {
        ...state,
        isLoading: true,
        error: null
      };

    case MoviesActionTypes.FetchCharacterMoviesSuccess:
      return {
        ...state,
        isLoading: false,
        selectedCharacterMovies: action.payload,
        // next: action.payload.next,
        // previous: action.payload.previous
      };

    case MoviesActionTypes.FetchCharacterMoviesError:
      return {
        ...state,
        isLoading: false,
        error: action.payload
      };

    case MoviesActionTypes.FetchMovieCharacters:
      return {
        ...state,
        isLoading: true,
        error: null
      };

    case MoviesActionTypes.FetchMovieCharactersSuccess:
      return {
        ...state,
        isLoading: false,
        selectedMovieCharacters: action.payload,
        // next: action.payload.next,
        // previous: action.payload.previous
      };


    case MoviesActionTypes.FetchMovieCharactersError:
      return {
        ...state,
        isLoading: false,
        error: action.payload
      };

    // case MoviesActionTypes.ChangePage:
    //   return {
    //     ...state,
    //     page: action.payload === Pagination.NEXT ? ++state.page : --state.page
    //   };

    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

we will create movie.action.ts

import { Action } from '@ngrx/store';
import { Movie, MoviesResponse } from './models/movie';
import { HttpErrorResponse } from '@angular/common/http';

export const enum MoviesActionTypes {

  FetchMovies = '[Movies] Fetch Movies',
  FetchMoviesSuccess = '[Movies] Load Movies Success',
  FetchMoviesError = '[Movies] Load Movies Error',

  ChangePage = '[Movies] Change page',
  FetchMovieCharacters = '[Movie] Fetch Movie Characters',
  FetchMovieCharactersSuccess = `[Movie] Load Movie Characters Success`,
  FetchMovieCharactersError = '[Movie] Load Movie Characters Error',
  FetchMovie = '[Movie] Fetch Movie ',
  FetchMovieSuccess = `[Movie] Load Movie Success`,
  FetchMovieError = '[Movie] Load Movie Error',

  FetchCharacter = '[Character] Fetch Character ',
  FetchCharacterSuccess = `[Character] Load Character Success`,
  FetchCharacterError = '[Character] Load Character Error',

  FetchCharacterMovies = '[Character] Fetch Character Movies ',
  FetchCharacterMoviesSuccess = `[Character] Load Character Movies Success`,
  FetchCharacterMoviesError = '[Character] Load Character Movies Error',

}

export const enum Pagination {
  NEXT,
  PREV
}

export class FetchMovies implements Action {
  readonly type = MoviesActionTypes.FetchMovies;
}

export class FetchMoviesSuccess implements Action {
  readonly type = MoviesActionTypes.FetchMoviesSuccess;

  constructor(public payload: Movie[]) { }
}

export class FetchMoviesError implements Action {
  readonly type = MoviesActionTypes.FetchMoviesError;

  constructor(public payload: HttpErrorResponse) { }
}
export class FetchMovie implements Action {
  readonly type = MoviesActionTypes.FetchMovie;
  constructor() {
    // console.log("*************FetchMovie*************");

  }
}

export class FetchCharacterSuccess implements Action {
  readonly type = MoviesActionTypes.FetchCharacterSuccess;

  constructor(public payload: any) {
    // console.log("FetchMovieSuccess");

  }
}

export class FetchCharacterError implements Action {
  readonly type = MoviesActionTypes.FetchCharacterError;

  constructor(public payload: HttpErrorResponse) { }
}

export class FetchCharacter implements Action {
  readonly type = MoviesActionTypes.FetchCharacter;
  constructor() {
    // console.log("*************FetchCharacter*************");

  }
}
export class FetchCharacterMoviesSuccess implements Action {
  readonly type = MoviesActionTypes.FetchCharacterMoviesSuccess;

  constructor(public payload: any) {


  }
}

export class FetchCharacterMoviesError implements Action {
  readonly type = MoviesActionTypes.FetchCharacterMoviesError;

  constructor(public payload: HttpErrorResponse) { }
}

export class FetchCharacterMovies implements Action {
  readonly type = MoviesActionTypes.FetchCharacterMovies;
  constructor() {
    // console.log("*************FetchCharacter*************");

  }
}
export class FetchMovieSuccess implements Action {
  readonly type = MoviesActionTypes.FetchMovieSuccess;

  constructor(public payload: any) {
    // console.log("FetchMovieSuccess");

  }
}

export class FetchMovieError implements Action {
  readonly type = MoviesActionTypes.FetchMovieError;

  constructor(public payload: HttpErrorResponse) { }
}

export class FetchMovieCharacters implements Action {
  readonly type = MoviesActionTypes.FetchMovieCharacters;
}

export class FetchMovieCharactersSuccess implements Action {
  readonly type = MoviesActionTypes.FetchMovieCharactersSuccess;


  constructor(public payload: any) {
    // console.log("FetchMovieCharactersSuccess");
  }
}

export class FetchMovieCharactersError implements Action {
  readonly type = MoviesActionTypes.FetchMovieCharactersError;

  constructor(public payload: HttpErrorResponse) { }
}



export class ChangePage implements Action {
  readonly type = MoviesActionTypes.ChangePage;

  constructor(public payload: Pagination) { }
}

export type MoviesActions = FetchMovieCharacters
  | FetchMovieCharactersSuccess
  | FetchMovieCharactersError
  | FetchMovies
  | FetchMoviesSuccess
  | FetchMoviesError
  | FetchMovie
  | FetchMovieSuccess
  | FetchMovieError
  | FetchCharacter
  | FetchCharacterSuccess
  | FetchCharacterError
  | FetchCharacterMovies
  | FetchCharacterMoviesSuccess
  | FetchCharacterMoviesError
  | ChangePage;

Enter fullscreen mode Exit fullscreen mode

For movie-list component , movie-detail component and other character components you can find complete project on this github link.
When you run project you can see list of "star wars" movies

Image description

After clicking to movie in list app shows movies details and list of characters

Image description

By clicking character name app will navigate to character details page, and it will show character details and list of movies where character exist.

Image description

and you can click movie name and it will navigate to movie details page.Application uses ngrx state to working all process with movies and characters.

Hope, this article help you to understand ngrx.

💖 💪 🙅 🚩
ilyoskhuja
Ilyoskhuja

Posted on April 2, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related