Howto navigate in Angular 2 when using ngrx store

Simon picture Simon · Sep 5, 2017 · Viewed 7.9k times · Source

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

Answer

Heehaaw picture Heehaaw · Sep 5, 2017

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,
  ) {}
}