Skip to content

4. Handling router state with @ngrx router store

aochagavia-infi edited this page Jan 23, 2019 · 4 revisions

Handling router state with @ngrx/router-store

In the exercise we want to store the search params in the url and find our flights based on the url params. We also will sync the url with our search form.

Before we will use the ngrx/router-store we try to implement the a custom way of making the url the source of truth.


Preconditions:

  • Install the @ngrx/router-store over npm and persist it to the package.json file.
    npm install --save @ngrx/router-store

  1. Custom implementation of url state

1.1 Subscribe to url changes

// app/pages/flight/components/search/search.component.ts

...
// add import
import {ActivatedRoute} from '@angular/router';

...
constructor(
    ...  
    // inject activated route
    private route: ActivatedRoute
  ) {

    ...
    // add subscription
    this.route.params.subscribe(
      (data: { from: string, to: string }) => {
        // find flights triggered by on ulr param change
        this.fs.find(data.from, data.to)
      }
    )

1.2 Trigger url change instead of flight search

...
import { ActivatedRoute, 
// add import
Router } from '@angular/router';

// search.component.ts
constructor(
    ...  
    // inject router
    private router: Router
  ) {

    ...

}
  searchFlights(form: FormGroup) {
    const data = form.value
    // this.fs.find(data.from, data.to)
    
    // trigger navigation
    this.router.navigate(['./', {from: data.from, to: data.to}])
  }

  ...

  refreshFlights() {
    // this.fs.find(null, null)
    this.router.navigate(['./', {from: null, to: null}])
  }

1.3 Sync the form search params with the url state

// app/pages/flight/components/search.component.ts

...
 constructor(
    private route: ActivatedRoute,
    private router: Router,
    private fb: FormBuilder,
    private fs: FlightService
  ) {
  ...
    this.route.params.subscribe(
      (data: { from: string, to: string }) => {
        // ensure that from and to are always provided
        const searchParams = Object.assign({from: '', to: ''}, data)
        this.searchForm.patchValue(searchParams)
        ...
      }
    )
  ...
  }
  1. ngrx/router-store implementation to sync with url params Now we will use the router-store to sync with url changes

2.1. connect router-store to routing actions

// app/app.module.ts

...
import * as fromRouter from '@ngrx/router-store';

export interface IDB {
  ...
  // extend state
  routerBranch: fromRouter.RouterReducerState<any>
}

const reducer = {
  ..., 
  // implement reducer
  routerBranch: fromRouter.routerReducer
}

@NgModule({
  ...
  imports: [
    ..., 
    // connect router-store
    fromRouter.StoreRouterConnectingModule
  ],
  ...
})
export class AppModule {
}

2.2. Listen to router actions and trigger find flights action

The router-store module, more accurate the router, will dispatch a ROUTER_NAVIGATION action. We can listen to it in any ´@Effect´.

// app/ngrx/flight.effects.ts

... 

@Injectable()
export class FlightEffects {

  find$: ...
  
  ...
  
  @Effect()
  // handle location update
  locationUpdate$: Observable<Action> = this.actions$.ofType('ROUTER_NAVIGATION')
    .filter((n: any) => {
      return n.payload.event.url.indexOf('flight')
    })
    .switchMap((action: any) => {
      // extract params from url
      const rS = action.payload.routerState
      const searchParams = rS.root.firstChild.params
      // trigger FindAction with search params
      return Observable.of(new flight.FindAction(searchParams))
    });
  ...
}

2.3. Remove manual calls to find flights

// app/pages/flight/components/search/search.component.ts 

...
  constructor(...) {
    ...
    this.route.params.subscribe(
      (data: { from: string, to: string }) => {
        const searchFormData = Object.assign({from: '', to: ''}, data)
        this.searchForm.patchValue(searchFormData)
        // remove find flights call
        // this.fs.find(data.from, data.to)
      }
    )
  ...
  }
...

Now you implemented ngrx-router-store to keep track of url state. Test your app!

Bonus Exercise: Implement a custom router state serializer

  1. Create the file router-state.serializer.ts and copy this code into it:
//app/ngrx/router-state.serializer.ts

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

export interface IRouterStateUrl {
  url: string;
  params: any;
}

export class CustomSerializer implements RouterStateSerializer<IRouterStateUrl> {

  serialize(routerState: RouterStateSnapshot): IRouterStateUrl {
    const { url } = routerState;
    const params = routerState.root.firstChild.params;

    console.log('routerState', routerState)

    // Only return an object including the URL and query params
    // instead of the entire snapshot
    return { url, params };
  }
}
  1. Adopt IDB interface with new IRouterStateUrl
// app/app.module.ts

...
import {CustomSerializer, IRouterStateUrl} from './ngrx/router-state.serializer';

export interface IDB {
  flightPage: IFlightState
  //routerReducer: fromRouter.RouterReducerState<any>
  routerReducer: fromRouter.RouterReducerState<IRouterStateUrl>
}
...
  1. Override provider for RouterStateSerializer with the CustomSerializer
// app/app.module.ts

...
import {CustomSerializer,
        // add import 
        IRouterStateUrl} from './ngrx/router-state.serializer';


...

@NgModule({
  ...
  providers: [
    {provide: RouterStateSerializer, useClass: CustomSerializer}
  ],
  ...
})
export class AppModule {
}
 
  1. Adopt locationUpdate$ for new RouterState
@Effect()
  // handle location update
  locationUpdate$: Observable<Action> = this.actions$.ofType('ROUTER_NAVIGATION')
    ...
    .switchMap((action: any) => {
      // const rS = action.payload.routerState
      // const searchParams = rS.root.firstChild.params
      const searchParams = action.payload.routerState.params
      return Observable.of(new flight.FindAction(searchParams))
    });

Congrats! You implemented a custom routerState serializer. Test it!