angularngrxngrx-effectsngrx-store-4.0

Howto navigate in Angular 2 when using ngrx store


I'm using ngrx store (4.x) together with Angular 4. I use effects to make CRUD operations on the backend, like the example below which adds a Task on the backend API.

Effect:

  @Effect()
  addTask: Observable<Action> = this.actions$
    .ofType(LeadAction.ADD_TASK)
    .map((action: LeadAction.AddTaskAction) => action.payload)
    .switchMap((task: TaskViewModel) => {
      return this.leadApi.leadAddTask(task.LeadId, task)
        .map((taskResult: TaskViewModel) => {
          return new LeadAction.AddTaskSuccessAction(taskResult);
        })
        .catch((e: any) => of(new LeadAction.AddTaskFailureAction(e)));
    });

TaskEditComponent :

  onSave(): void {
    this.store.dispatch(new AddTaskAction(this.task));

    // **** NAVIGATE TO PAGE TaskListComponent or OverviewComponent ON SUCCESS
    // OR
    // **** NAVGIATE TO PAGE Y ON ERROR
  }

Question: In my component I need to navigate to different pages and I struggle now where to put this logic?

Especially when I think about following scenarios, where the TaskEditComponent is 'called' by different Components:

Should navigate back to TaskListComponent:

OverviewComponent->TaskListComponent->TaskEditComponent back to List

Should navigate back to OverviewComponent:

OverviewComponent->TaskEditComponent


Solution

  • Using ngrx, it makes sense to let your store handle the router state as well, preserving the redux paradigm. Then you would simply dispatch a router action in the effect in reaction to your success actions.

    This has the added benefit of being able to 'time travel' the routes as well as the rest of the app state.

    Fortunately, there is already an implementation of router-store integration ready to be used.


    You could do something like this (just a guideline, enhance to your needs):

    app.module

    import { StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store';
    import { App } from './app.component';
    
    @NgModule({
      imports: [
        BrowserModule,
        StoreModule.forRoot({ routerReducer: routerReducer }),
        RouterModule.forRoot([
          // ...
          { path: 'task-list', component: TaskListComponent },
          { path: 'error-page', component: ErrorPageComponent }
        ]),
        StoreRouterConnectingModule
      ],
      bootstrap: [App]
    })
    export class AppModule { }
    

    task.effects

    import { go } from '@ngrx/router-store';
    
    @Effect()
    addTask: Observable<Action> = this.actions$
      .ofType(LeadAction.ADD_TASK_SUCCESS)
      .map((action: LeadAction.AddTaskSuccessAction) => action.payload)
      .map((payload: any) => go('/task-list')); // use payload to construct route options
    
    @Effect()
    addTask: Observable<Action> = this.actions$
      .ofType(LeadAction.ADD_TASK_FAILURE)
      .mapTo(go('/error-page'));
    

    Update using NGRX v8+ with latest features:

    AppModule:

    import { StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store';
    import { AppComponent } from './app.component';
    
    @NgModule({
      imports: [
        BrowserModule,
        StoreModule.forRoot({ routerReducer }),
        RouterModule.forRoot([
          // ...
          { path: 'task-list', component: TaskListComponent },
          { path: 'error-page', component: ErrorPageComponent }
        ]),
        StoreRouterConnectingModule.forRoot(),
      ],
      bootstrap: [AppComponent],
    })
    export class AppModule {}
    

    TaskEffects:

    @Injectable()
    export class TaskEffects {
      readonly addTaskSuccess$ = createEffect(() =>
        this.actions$.pipe(
          ofType(LeadAction.ADD_TASK_SUCCESS),
          tap(() => this.router.navigate(['task-list'])),
        ),
        { dispatch: false },
      );
      readonly addTaskFailure$ = createEffect(() =>
        this.actions$.pipe(
          ofType(LeadAction.ADD_TASK_FAILURE),
          tap(() => this.router.navigate(['error-page'])),
        ),
        { dispatch: false },
      );
    
      constructor(
        private readonly actions$: Actions,
        private readonly router: Router,
      ) {}
    }