Search code examples
angularobservablengrxngrx-effects

NgRx Effects infinite loop


I know that this question has been asked a million times on SO. But I need help. I do realise I must be missing something crucial here. But I'm not seeing it.

Below code triggers an infinite loop. REMOVE_PROJECT is being dispatched only once. REMOVE_PROJECT_SUCCESS is being triggered infinitely. 'removed' is being logged infinitely. I have no idea why.

All Actions have unique types. dispatch: false is enabled for REMOVE_PROJECT_SUCCESS.

Actions:

export const REMOVE_PROJECT = createAction(
    '[Project] Remove Project',
    props<{ id: string }>()
);

export const REMOVE_PROJECT_SUCCESS = createAction(
    '[Project] Remove Project Success',
);

Effects:

@Effect()
removeProject$ = createEffect(() => this.actions$.pipe(
    ofType(ProjectActions.REMOVE_PROJECT),
    switchMap(props =>
        this.projects.removeProject(props.id).pipe(
           map(() => ({ type: '[Project] Remove Project Success'}),
        // have also tried
        // map(() => ProjectActions.REMOVE_PROJECT_SUCCESS())
        )
    ))
))


@Effect({ dispatch: false })
removeProjectSuccess$ = createEffect(() => this.actions$.pipe(
    ofType(ProjectActions.REMOVE_PROJECT_SUCCESS),
    tap(() => console.log('removed')),
))

Delete function:

removeProject(projectId): Observable<void> {
    return from(this.db.doc('projects/' + projectId).ref.delete());
}

Solution

  • This is because you're using the @Effect() decorator with createEffect(). They both do the same thing under the hood, likely triggering the infinite loop. Remove EITHER the @Effect() annotations (recommended) or createEffect() (I'd leave this one).

    To be more specific, the createEffect() also takes {dispatch: false} as the second argument (after your observable pipe). Since you don't include this option for the second one, it just re-dispatches the actions you filtered with ofType, thereby firing the same actions over and over in the infinite loop you're experiencing.

    Here's what the second effect should look like if you want to not dispatch:

    removeProjectSuccess$ = createEffect(() => this.actions$.pipe(
        ofType(ProjectActions.REMOVE_PROJECT_SUCCESS),
        tap(() => console.log('removed')),
    ), { dispatch: false })
    

    Under the hood, both the decorator and the function add annotations to the injectable class that the EffectsModule consumes. In this scenario, it makes an effect that will not dispatch, and a second one that dispatches by default. I hope this makes sense.