Amruta
Amruta

Reputation: 1575

NgRx/Effects not triggering the Reducer

I know the title of the question is vague, but unfortunately I don't exactly know what is causing my issue so please bear with me.

I just started working on a small Angular 8 task, which uses NgRx/Store and NgRx/Effects as well. I am fetching an array of Book objects using a getBooks function. My codes are as follows(Haven't put inports everywhere just to make the code parts shorter)

book.component.ts

export class BookComponent {

  constructor(private booksService: BooksService, public store: Store<BooksState>) {
    this.books$ = store.pipe(select('books'));
  }

  books$: Observable<BooksState>;
  BooksSubscription: Subscription;
  booksList: Book[];

  ngOnInit() {
    this.BooksSubscription = this.books$
      .pipe( map(x => { this.booksList = x.Books; }) ).subscribe();
    this.store.dispatch(BooksActions.BeginGetBooksAction())
  }
}

book.service.ts

import { Injectable } from '@angular/core';
import booksData from "../../books.json";

@Injectable()
export class BooksService {
  constructor() { }

  getBooks(){
      return booksData;
  }
}

booksData is an array of objects of type Book which I import from a json file.

book.action.ts

import { createAction, props } from "@ngrx/store";
import { Book } from "./book";

export const GetBooksAction = createAction("GetBooks");
export const BeginGetBooksAction = createAction("BeginGetBooks");
export const SuccessGetBooksAction = createAction( "SuccessGetBooks", props<{ payload: Book[] }>());

book.reducer.ts

import { Action, createReducer, on } from "@ngrx/store";
import * as BooksActions from "./book.action";
import { Book } from "./book";

export interface BooksState {
  Books: Book[];
}

const initialState: BooksState = {
    Books: []
};

const booksReducer = createReducer(
    initialState,
    on(BooksActions.GetBooksAction, state => state))
    on(BooksActions.SuccessGetBooksAction, (state: BooksState, { payload }) => {
      console.log(payload)
      return { ...state, Books: payload };
    });

  export function reducer(state: BooksState | undefined, action: Action) {
    return booksReducer(state, action);
  }

book.effect.ts

Now this is where I'm lost. I found a reference code on the NgRx docs based on which I wrote my code but it does not work

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map, mergeMap} from 'rxjs/operators';
import * as BooksActions from './book.action';
import { Book } from './book';
import { BooksService } from './book.service';


@Injectable()
export class BooksEffects {
    constructor(private action$: Actions, private booksService: BooksService) { }

     GetBooks$: Observable<Action> = createEffect(() =>
        this.action$.pipe(
            ofType(BooksActions.BeginGetBooksAction),
            mergeMap(
                () => this.booksService.getBooks().pipe(
                    map((booksData: Book[]) => {
                        return BooksActions.SuccessGetBooksAction({ payload: booksData });
                    }))
            )
        )
    )
  );
}

I get following errors during compilation. The errors are on the lines where I have pipe functions

error TS2322: Type 'Observable<unknown>' is not assignable to type 'Observable<Action> | ((...args: any[]) 
=> Observable<Action>)'.
      Type 'Observable<unknown>' is not assignable to type 'Observable<Action>'.
        Property 'type' is missing in type '{}' but required in type 'Action'.

error TS2339: Property 'pipe' does not exist on type '{ "isbn": string; "title": string; "subtitle": string; "author": string; "published": string; "publisher": string; "pages": number; "description": string; "website": string; }[]'.

Also, I have added StoreModule.forRoot({ books: reducer}) and EffectsModule.forRoot([BooksEffects]) to imports and added BookService to Providers in app.module.ts

Upvotes: 2

Views: 3510

Answers (2)

pascalpuetz
pascalpuetz

Reputation: 5428

There are two mistakes in your code.

MergeMap used incorrectly

Your this.booksService.getBooks() seems not to return an Observable. In this case, you want to use map instead of mergeMap:

GetBooks$: Observable<Action> = createEffect(() =>
   this.action$.pipe(
      ofType(BooksActions.BeginGetBooksAction),
      map(() => this.booksService.getBooks()),
      map(payload => BooksActions.SuccessGetBooksAction({ payload })                
   )
);

To elaborate on this, mergeMap is used when you want to switch from one Observable to another (similiar to switchMap). The callback is expected to return an Observable to which mergeMap can then subscribe. The callback for map is expected to return a value and wraps that value into a new Observable for you. As you getBooks() does not return an Observable, mergeMap cannot subscribe to it, TypeScript knows that and throws said errors. Alternatively, you could also change your getBooks() to return an Observable instead (this would be the way to go if you later want to fetch these from an HTTP Request, which would return an Observable.

Wrong parenthesis in reducer

Also, on further inspection, your effects and your reducer are actually running. But you have a small mistake with your paranthesis in your reducer. Look at this:

const booksReducer = createReducer(
    initialState,
    on(BooksActions.GetBooksAction, state => state))  // <---- This ")" is closing your "createReducer" function!
    on(BooksActions.SuccessGetBooksAction, (state: BooksState, { payload }) => {
      console.log(payload)
      return { ...state, Books: payload };
    });

so if you format this to reflect what it's doing you wrote:

const booksReducer = createReducer(
    initialState,
    on(BooksActions.GetBooksAction, state => state)
)

// You "on" function is actually outside "createReducer" effectively NEVER being triggered (but this is syntactically correct so a very tricky error)
on(BooksActions.SuccessGetBooksAction, (state: BooksState, { payload }) => {
   console.log(payload)
   return { ...state, Books: payload };
});

So you just need to fix that one:

const booksReducer = createReducer(
    initialState,
    on(BooksActions.GetBooksAction, state => state),
    on(BooksActions.SuccessGetBooksAction, (state: any, { payload }) => {
      console.log('Reducer is running, value is:', payload)
      return { ...state, Books: payload };
    })
);

Here is a working Stackblitz. Look into the console to see everything being triggered.

Upvotes: 5

sloth
sloth

Reputation: 101142

pipe is a function of Observable, but this.booksService.getBooks() returns a plain Array instead.

So either don't call pipe on the resulting Array or let your service return an Observable. An easy way is to return of( booksData); the of function creates an Observable from the given value.

Upvotes: 1

Related Questions