DEV Community

Cover image for How to implement ngrx-router-store
Salim Chemes
Salim Chemes

Posted on • Updated on

How to implement ngrx-router-store

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';
Enter fullscreen mode Exit fullscreen mode

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,
    }),
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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 };
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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
);

Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (3)

Collapse
 
rainocode profile image
rainocode

In this detailed guide, we'll delve into the implementation of ngrx-router-store, a powerful library that seamlessly integrates Angular's router with NgRx for state management. By leveraging ngrx-router-store, you can synchronize router state with your NgRx store, enabling centralized management of navigation-related data and facilitating enhanced routing features. Here's what we'll cover:

rainocode.com/blog/how-to-implemen...

Collapse
 
mmcgeeaya profile image
Michael McGee

I won't lie, this is sort of confusing, but probably due to my ignorance. Thanks for providing it, I will digest it again. My confusion is around perhaps there being it own router-state module.

Collapse
 
ozgursarikamis profile image
Özgür SARIKAMIŞ

neat explanation! Thanks