1
votes

I know this has been asked many times before, but i am still not able to understand what is going wrong with my code. It seems like my effects have taken up steroids and are simply not stopping from running endlessly. I am on ngrx/store and ngrx/effects version 10.1.1

enter image description here

Below is the code:

home.effects.ts

  public defaultState: DeployedInstanceModel;

  @Effect()
  loadDeployedInstanceData$ = this.actions$
    .pipe(
      
      ofType(DeployedInstancesTypes.GET_TABLE_DATA),
      
      mergeMap(() => {
        // reset default state before api call is made
        this.defaultState = {
          data: {}
        };
        return this.analyticsService.getDeployedInstanceData().pipe(
            map(result => {
                this.defaultState.data = result;
                console.log('[deployed-instance] the result of api call is -> ', result);
                return new DeployedInstanceGetData.GetDeployedInstanceAction(this.defaultState);
            }),
            catchError(err => {
              let errObj = {err: '', url: ''}
              errObj.err = err.statusText;
              errObj.url = err.url;
              console.log('API failure detected on the url -> ', errObj.url);
              this.showErrorPopup();
              return errObj.err;
            })
        );
      })
    )

  constructor(
    private actions$: Actions,
    private analyticsService: AnalyticsService
  ) { }

and here is how I am trying to dispatch my actions from home component

home.component.ts

ngOnInit(){

    this.state = { data: {}};

    // first dispatch empty state on page load
    this.store.dispatch(new DeployedInstanceGetData.GetDeployedInstanceAction(this.state));
    
    // get the data from the store once api has been called from effects
    this.homeDeployedInstanceStore$ = this.store.select('deployedInstanceModel').subscribe(state => {
      this.totalData = state.data;
      if(Object.keys(this.totalData).length > 0){
          console.log(this.totalData);
          this.loader.stop();
      }else{
        this.loader.start();
      }
      console.log('[Deployed Instance] state from component -> ', this.totalData);
    });
}

What I tried:

I tried all the solutions mentioned below:

@Effect({dispatch:false}) - Here data gets extracted once but then my this.store.select is not getting called again when I have the data which returns in empty data shown on home page

take(1) - tried with this solution but then I am not able to call api the second time when user navigates out of the page and comes back again on the same page.

tried removing @Effect but again same problem I am getting.

It would be really helpful if someone can point me in the right direction.

2
you are dispatching same action in map operator DeployedInstanceGetData.GetDeployedInstanceAction after effect .. this making a recursion. You should return another action for example DeployedInstanceGetData.GetDeployedInstanceSuccessAction which will have new effect or do nothing - Franky238
@Franky238 Oh Okay, thanks for pointing it out. Let me try with your solution - Dhruvify
@Franky238 thank you very much. It worked as per what you said. I have updated the details in my answer posted below as solution. - Dhruvify

2 Answers

1
votes

I was finally able to solve this problem as per what user Franky238 mentioned in the comment:

The problem with my code was that I was returning the same action in effects file i.e. new DeployedInstanceGetData.GetDeployedInstanceAction that was initially dispatched from home.component.ts. That made my code to continuously dispatch action resulting in an infinite loop. I have changed my code in this manner:

home.effects.ts

The code inside switchmap now looks like given below. I've changed return new DeployedInstanceGetData.GetDeployedInstanceAction(this.defaultState); with return new DeployedInstanceGetData.DataFetchSuccessAction(this.defaultState);

return this.analyticsService.getDeployedInstanceData().pipe(
  map(result => {
    this.defaultState.data = result.data;
    console.log('[deployed-instance] the result of api call is -> ', this.defaultState.data);
    return new DeployedInstanceGetData.DataFetchSuccessAction(this.defaultState);
  }),
  catchError(err => {
    let errObj = {err: '', url: ''}
    errObj.err = err.statusText;
    errObj.url = err.url;
    console.log('API failure detected on the url -> ', errObj.url);
    this.showErrorPopup();
    return errObj.err;
  })
);

home.actions.ts

export const GET_DEPLOYEMENT_DATA = DeployedInstancesTypes.GET_TABLE_DATA;
export const DEPLOYEMENT_DATA_FETCH_SUCCESS = DeployedInstancesTypes.TABLE_FETCH_SUCCESS;

export class GetDeployedInstanceAction implements Action{
  readonly type = GET_DEPLOYEMENT_DATA;
  constructor(public payload: DeployedInstanceModel) {
    console.log('[result] action with payload =>', payload);
  }
}

// added this new action to accommodate new effect and 
// save data in the store below.

export class DataFetchSuccessAction implements Action{
  readonly type = DEPLOYEMENT_DATA_FETCH_SUCCESS;
  constructor(public payload: DeployedInstanceModel) {
    console.log('[result] action dispatch from effects success =>', payload);
  }
}

export type Actions = GetDeployedInstanceAction | DataFetchSuccessAction
1
votes

My advice is create 3 Actions for each resolve of async request. example: FetchData, FetchDataSuccess, FetchDataFailure. BUT! If want to handle action in catchError you need to emit new observable.

Example from my project:

export const FetchDraftAction = createAction(
  '[GOALIE_TRACKER.TRACKER_DETAIL] FETCH_DRAFT',
  props<{ uuid: string }>(),
);

export const FetchDraftSuccessAction = createAction(
  '[GOALIE_TRACKER.TRACKER_DETAIL] FETCH_DRAFT_SUCCESS',
  props<{ draft: DraftRecord }>(),
);

export const FetchDraftFailureAction = createAction(
  '[GOALIE_TRACKER.TRACKER_DETAIL] FETCH_DRAFT_FAILURE',
);

And do so in effect:

  public fetchDraft$ = createEffect(() =>
    this.actions$.pipe(
      ofType(FetchDraftAction),
      switchMap(({ uuid }) =>
        this.matchStatService.getDraftByKey(uuid).pipe(
          map(draft => FetchDraftSuccessAction({ draft })),
          catchError(() => of(FetchDraftFailureAction())),
        ),
      ),
    ),
  );

  public fetchDraftSuccess$ = createEffect(() =>
    this.actions$.pipe(
      ofType(FetchDraftSuccessAction),
      switchMap(() => [StopLoadingMessageAction()]),
    ),
  );
  public fetchDraftFailure$ = createEffect(() =>
    this.actions$.pipe(
      ofType(FetchDraftFailureAction),
      switchMap(() => [StopLoadingMessageAction()]),
    ),
  );

As you can see there is catchError(() => of(FetchDraftFailureAction())), in fetchDraft$. It is starting with of(...) operator. Thats because your effect will break stream on error and you need to "start it up" again with new action.

That's my suggestion. Enjoy!

PS: I am using action and effect creators but approach is same.