loading...
Cover image for How to implement ngrx-router-store

How to implement ngrx-router-store

salimchemes profile image Salim Chemes Updated on ・4 min read

Nowadays NgRx is a very popular framework mostly used when having an app with complex/shared state.
This is the list of packages offered by the framework today:

  • Store: RxJS powered state management for Angular apps, inspired by Redux.
  • Store Devtools: Instrumentation for @ngrx/store enabling time-travel debugging.
  • Effects: Side effect model for @ngrx/store.
  • Router Store: Bindings to connect the Angular Router to @ngrx/store.
  • Entity: Entity State adapter for managing record collections.
  • NgRx Data: Extension for simplified entity data management.
  • NgRx Component: Extension for fully reactive, fully zone-less applications.
  • ComponentStore: Standalone library for managing local/component state.
  • Schematics: Scaffolding library for Angular applications using NgRx libraries.

For more details you can check the docs

In this post, we will implement Router Store, step by step.

Why do we need Router Store? Basically to link the routing with the NgRx store. Every time the router changes, an action will be dispatched and will update the store through a reducer.

Alt Text

We will divide the implementation in 4 steps, with an example of a list of movies and series:

1. Add required dependencies
2. Update app.module.ts
3. Create router reducer and Custom Router State Serializer
4. Create a selector and subscribe from a component

1. Add required dependencies
npm install @ngrx/router-store --save

2. Update app.module.ts
We need to

 import { StoreRouterConnectingModule } from '@ngrx/router-store';

We import StoreRouterConnectingModule to connect RouterModule with StoreModule, which has a serializer class named CustomSerializer, we will cover this in step #3

    StoreRouterConnectingModule.forRoot({
      serializer: CustomSerializer,
    }),

Assuming we have already implemented the Store and StoreDevtoolsModule, this is how our app.module.ts looks

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { MoviesDetailComponent } from './pages/movies-detail/movies-detail.component';
import { MoviesComponent } from './pages/movies/movies.component';
import { SeriesDetailComponent } from './pages/series-detail/series-detail.component';
import { SeriesComponent } from './pages/series/series.component';
import { CustomSerializer } from './store/custom-serializer';
import { reducers } from './store/index';

@NgModule({
  declarations: [
    AppComponent,
    MoviesComponent,
    SeriesComponent,
    SeriesDetailComponent,
    MoviesDetailComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    StoreModule.forRoot(reducers),
    StoreDevtoolsModule.instrument({
      maxAge: 25, // Retains last 25 states
      logOnly: true, // Restrict extension to log-only mode
    }),
    StoreRouterConnectingModule.forRoot({
      serializer: CustomSerializer,
    }),
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

3. Create router reducer and Custom Router State Serializer
Let's create CustomSerializer class we set in app.module.ts, we want to just return some params and not the entire snapshot object to avoid possible performance issues

import { Params, RouterStateSnapshot } from '@angular/router';
import { RouterStateSerializer } from '@ngrx/router-store';

export interface RouterStateUrl {
  url: string;
  params: Params;
  queryParams: Params;
}

export class CustomSerializer implements RouterStateSerializer<RouterStateUrl> {
  serialize(routerState: RouterStateSnapshot): RouterStateUrl {
    let route = routerState.root;

    while (route.firstChild) {
      route = route.firstChild;
    }

    const {
      url,
      root: { queryParams },
    } = routerState;
    const { params } = route;

    // Only return an object including the URL, params and query params
    // instead of the entire snapshot
    return { url, params, queryParams };
  }
}

And finally we add our router reducer

import { ActionReducerMap } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
import { routerReducer } from '@ngrx/router-store';

export interface StoreRootState {
  router: fromRouter.RouterReducerState<any>;
}
export const reducers: ActionReducerMap<StoreRootState> = {
  router: routerReducer,
};

4. Create a selector and subscribe from a component
We have it all set, the last step is to add a selector and subscribe to it from a component
Creating a selector

import * as fromRouter from '@ngrx/router-store';
import { createSelector } from '@ngrx/store';
import { StoreRootState } from '.';

export const getRouterState = (state: StoreRootState) => state.router;

export const getCurrentRouteState = createSelector(
  getRouterState,
  (state: fromRouter.RouterReducerState) => state.state
);

Subscribing from a component

import { Component, OnDestroy, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { series } from 'src/app/app.constants';
import { StoreRootState } from 'src/app/store';
import { getCurrentRouteState } from 'src/app/store/selectors';

@Component({
  selector: 'app-series-detail',
  templateUrl: './series-detail.component.html',
  styleUrls: ['./series-detail.component.scss'],
})
export class SeriesDetailComponent implements OnInit, OnDestroy {
  seriesId: string;
  series;
  private subscriptions: { [key: string]: any } = {};

  constructor(private store: Store<StoreRootState>) {}

  ngOnInit(): void {
    this.subscriptions.routerSelector = this.store
      .pipe(select(getCurrentRouteState))
      .subscribe((route: any) => {
        const seriesId = route.params.seriesId;
        this.series = series.find((series) => series.id === seriesId);
      });
  }

  ngOnDestroy(): void {
    this.subscriptions.routerSelector.unsubscribe();
  }
}

The coding part is done, let's see how the example works

This is how the store looks when the app starts
Alt Text

Let's navigate to the series list and see what happens in the store
Alt Text

One more navigation to notice that route state has changed, including url and params
Alt Text

Thanks for reading!

References

Discussion

pic
Editor guide
Collapse
ozgursarikamis profile image
Özgür SARIKAMIŞ

neat explanation! Thanks