Skip to content

Commit 858de40

Browse files
feat(RouterStore): Added serializer for router state snapshot
This adds a serializer that can be customized for returning the router state. By default, the entire RouterStateSnapshot is returned. A custom serializer can be provided to parse the snapshot into a more managable structure. Closes #104, #97
1 parent 7d23fdb commit 858de40

File tree

9 files changed

+191
-18
lines changed

9 files changed

+191
-18
lines changed

docs/router-store/README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ Install @ngrx/router-store from npm:
1515
During the navigation, before any guards or resolvers run, the router will dispatch a ROUTER_NAVIGATION action, which has the following signature:
1616

1717
```ts
18-
export type RouterNavigationPayload = {
19-
routerState: RouterStateSnapshot,
18+
export type RouterNavigationPayload<T> = {
19+
routerState: T,
2020
event: RoutesRecognized
2121
}
2222
```
@@ -46,3 +46,6 @@ import { App } from './app.component';
4646
})
4747
export class AppModule { }
4848
```
49+
50+
## API Documentation
51+
- [Custom Router State Serializer](./api.md#custom-router-state-serializer)

docs/router-store/api.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# API
2+
3+
## Custom Router State Serializer
4+
5+
During each navigation cycle, a `RouterNavigationAction` is dispatched with a snapshot of the state in its payload, the `RouterStateSnapshot`. The `RouterStateSnapshot` is a large complex structure, containing many pieces of information about the current state and what's rendered by the router. This can cause performance
6+
issues when used with the Store Devtools. In most cases, you may only need a piece of information from the `RouterStateSnapshot`. In order to pair down the `RouterStateSnapshot` provided during navigation, you provide a custom serializer for the snapshot to only return what you need to be added to the payload and store.
7+
8+
To use the time-traveling debugging in the Devtools, you must return an object containing the `url` when using the `routerReducer`.
9+
10+
```ts
11+
import { StoreModule } from '@ngrx/store';
12+
import {
13+
StoreRouterConnectingModule,
14+
routerReducer,
15+
RouterStateSerializer,
16+
RouterStateSnapshotType
17+
} from '@ngrx/router-store';
18+
19+
export interface RouterStateUrl {
20+
url: string;
21+
}
22+
23+
export class CustomSerializer implements RouterStateSerializer<RouterStateUrl> {
24+
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
25+
const { url } = routerState;
26+
27+
// Only return an object including the URL
28+
// instead of the entire snapshot
29+
return { url };
30+
}
31+
}
32+
33+
@NgModule({
34+
imports: [
35+
StoreModule.forRoot({ routerReducer: routerReducer }),
36+
RouterModule.forRoot([
37+
// routes
38+
]),
39+
StoreRouterConnectingModule
40+
],
41+
providers: [
42+
{ provide: RouterStateSerializer, useClass: CustomSerializer }
43+
]
44+
})
45+
export class AppModule { }
46+
```

example-app/app/app.module.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { HttpModule } from '@angular/http';
88
import { StoreModule } from '@ngrx/store';
99
import { EffectsModule } from '@ngrx/effects';
1010
import { DBModule } from '@ngrx/db';
11-
import { StoreRouterConnectingModule } from '@ngrx/router-store';
11+
import {
12+
StoreRouterConnectingModule,
13+
RouterStateSerializer,
14+
} from '@ngrx/router-store';
1215
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
1316

1417
import { CoreModule } from './core/core.module';
@@ -17,6 +20,7 @@ import { AuthModule } from './auth/auth.module';
1720
import { routes } from './routes';
1821
import { reducers, metaReducers } from './reducers';
1922
import { schema } from './db';
23+
import { CustomRouterStateSerializer } from './shared/utils';
2024

2125
import { AppComponent } from './core/containers/app';
2226
import { environment } from '../environments/environment';
@@ -74,6 +78,14 @@ import { environment } from '../environments/environment';
7478

7579
AuthModule.forRoot(),
7680
],
81+
providers: [
82+
/**
83+
* The `RouterStateSnapshot` provided by the `Router` is a large complex structure.
84+
* A custom RouterStateSerializer is used to parse the `RouterStateSnapshot` provided
85+
* by `@ngrx/router-store` to include only the desired pieces of the snapshot.
86+
*/
87+
{ provide: RouterStateSerializer, useClass: CustomRouterStateSerializer },
88+
],
7789
bootstrap: [AppComponent],
7890
})
7991
export class AppModule {}

example-app/app/reducers/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ActionReducer,
66
} from '@ngrx/store';
77
import { environment } from '../../environments/environment';
8+
import * as fromRouter from '@ngrx/router-store';
89

910
/**
1011
* Every reducer module's default export is the reducer function itself. In
@@ -21,6 +22,7 @@ import * as fromLayout from '../core/reducers/layout';
2122
*/
2223
export interface State {
2324
layout: fromLayout.State;
25+
routerReducer: fromRouter.RouterReducerState;
2426
}
2527

2628
/**
@@ -30,6 +32,7 @@ export interface State {
3032
*/
3133
export const reducers: ActionReducerMap<State> = {
3234
layout: fromLayout.reducer,
35+
routerReducer: fromRouter.routerReducer,
3336
};
3437

3538
// console.log all actions

example-app/app/shared/utils.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { RouterStateSerializer } from '@ngrx/router-store';
2+
import { RouterStateSnapshot } from '@angular/router';
3+
4+
/**
5+
* The RouterStateSerializer takes the current RouterStateSnapshot
6+
* and returns any pertinent information needed. The snapshot contains
7+
* all information about the state of the router at the given point in time.
8+
* The entire snapshot is complex and not always needed. In this case, you only
9+
* need the URL from the snapshot in the store. Other items could be
10+
* returned such as route parameters, query parameters and static route data.
11+
*/
12+
13+
export interface RouterStateUrl {
14+
url: string;
15+
}
16+
17+
export class CustomRouterStateSerializer
18+
implements RouterStateSerializer<RouterStateUrl> {
19+
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
20+
const { url } = routerState;
21+
22+
return { url };
23+
}
24+
}

modules/router-store/spec/integration.spec.ts

+54-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Component } from '@angular/core';
1+
import { Component, Provider } from '@angular/core';
22
import { TestBed } from '@angular/core/testing';
3-
import { NavigationEnd, Router } from '@angular/router';
3+
import { NavigationEnd, Router, RouterStateSnapshot } from '@angular/router';
44
import { RouterTestingModule } from '@angular/router/testing';
55
import { Store, StoreModule } from '@ngrx/store';
66
import {
@@ -10,6 +10,7 @@ import {
1010
RouterAction,
1111
routerReducer,
1212
StoreRouterConnectingModule,
13+
RouterStateSerializer,
1314
} from '../src/index';
1415
import 'rxjs/add/operator/filter';
1516
import 'rxjs/add/operator/first';
@@ -315,11 +316,60 @@ describe('integration spec', () => {
315316
]);
316317
done();
317318
});
319+
320+
it('should support a custom RouterStateSnapshot serializer ', done => {
321+
const reducer = (state: any, action: RouterAction<any>) => {
322+
const r = routerReducer(state, action);
323+
return r && r.state
324+
? { url: r.state.url, navigationId: r.navigationId }
325+
: null;
326+
};
327+
328+
class CustomSerializer implements RouterStateSerializer<{ url: string }> {
329+
serialize(routerState: RouterStateSnapshot) {
330+
const url = `${routerState.url}-custom`;
331+
332+
return { url };
333+
}
334+
}
335+
336+
const providers = [
337+
{ provide: RouterStateSerializer, useClass: CustomSerializer },
338+
];
339+
340+
createTestModule({ reducers: { routerReducer, reducer }, providers });
341+
342+
const router = TestBed.get(Router);
343+
const store = TestBed.get(Store);
344+
const log = logOfRouterAndStore(router, store);
345+
346+
router
347+
.navigateByUrl('/')
348+
.then(() => {
349+
log.splice(0);
350+
return router.navigateByUrl('next');
351+
})
352+
.then(() => {
353+
expect(log).toEqual([
354+
{ type: 'router', event: 'NavigationStart', url: '/next' },
355+
{ type: 'router', event: 'RoutesRecognized', url: '/next' },
356+
{ type: 'store', state: { url: '/next-custom', navigationId: 2 } },
357+
{ type: 'router', event: 'NavigationEnd', url: '/next' },
358+
]);
359+
log.splice(0);
360+
done();
361+
});
362+
});
318363
});
319364
});
320365

321366
function createTestModule(
322-
opts: { reducers?: any; canActivate?: Function; canLoad?: Function } = {}
367+
opts: {
368+
reducers?: any;
369+
canActivate?: Function;
370+
canLoad?: Function;
371+
providers?: Provider[];
372+
} = {}
323373
) {
324374
@Component({
325375
selector: 'test-app',
@@ -361,6 +411,7 @@ function createTestModule(
361411
provide: 'CanLoadNext',
362412
useValue: opts.canLoad || (() => true),
363413
},
414+
opts.providers || [],
364415
],
365416
});
366417

modules/router-store/src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ export {
1313
RouterNavigationPayload,
1414
StoreRouterConnectingModule,
1515
} from './router_store_module';
16+
17+
export {
18+
RouterStateSerializer,
19+
DefaultRouterStateSerializer,
20+
} from './serializer';

modules/router-store/src/router_store_module.ts

+28-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
} from '@angular/router';
99
import { Store } from '@ngrx/store';
1010
import { of } from 'rxjs/observable/of';
11-
11+
import {
12+
DefaultRouterStateSerializer,
13+
RouterStateSerializer,
14+
} from './serializer';
1215
/**
1316
* An action dispatched when the router navigates.
1417
*/
@@ -17,17 +20,17 @@ export const ROUTER_NAVIGATION = 'ROUTER_NAVIGATION';
1720
/**
1821
* Payload of ROUTER_NAVIGATION.
1922
*/
20-
export type RouterNavigationPayload = {
21-
routerState: RouterStateSnapshot;
23+
export type RouterNavigationPayload<T> = {
24+
routerState: T;
2225
event: RoutesRecognized;
2326
};
2427

2528
/**
2629
* An action dispatched when the router navigates.
2730
*/
28-
export type RouterNavigationAction = {
31+
export type RouterNavigationAction<T> = {
2932
type: typeof ROUTER_NAVIGATION;
30-
payload: RouterNavigationPayload;
33+
payload: RouterNavigationPayload<T>;
3134
};
3235

3336
/**
@@ -78,7 +81,7 @@ export type RouterErrorAction<T> = {
7881
* An union type of router actions.
7982
*/
8083
export type RouterAction<T> =
81-
| RouterNavigationAction
84+
| RouterNavigationAction<T>
8285
| RouterCancelAction<T>
8386
| RouterErrorAction<T>;
8487

@@ -133,7 +136,7 @@ export function routerReducer(
133136
* declarations: [AppCmp, SimpleCmp],
134137
* imports: [
135138
* BrowserModule,
136-
* StoreModule.provideStore(mapOfReducers),
139+
* StoreModule.forRoot(mapOfReducers),
137140
* RouterModule.forRoot([
138141
* { path: '', component: SimpleCmp },
139142
* { path: 'next', component: SimpleCmp }
@@ -146,16 +149,24 @@ export function routerReducer(
146149
* }
147150
* ```
148151
*/
149-
@NgModule({})
152+
@NgModule({
153+
providers: [
154+
{ provide: RouterStateSerializer, useClass: DefaultRouterStateSerializer },
155+
],
156+
})
150157
export class StoreRouterConnectingModule {
151-
private routerState: RouterStateSnapshot | null = null;
158+
private routerState: RouterStateSnapshot;
152159
private storeState: any;
153160
private lastRoutesRecognized: RoutesRecognized;
154161

155162
private dispatchTriggeredByRouter: boolean = false; // used only in dev mode in combination with routerReducer
156163
private navigationTriggeredByDispatch: boolean = false; // used only in dev mode in combination with routerReducer
157164

158-
constructor(private store: Store<any>, private router: Router) {
165+
constructor(
166+
private store: Store<any>,
167+
private router: Router,
168+
private serializer: RouterStateSerializer<RouterStateSnapshot>
169+
) {
159170
this.setUpBeforePreactivationHook();
160171
this.setUpStoreStateListener();
161172
this.setUpStateRollbackEvents();
@@ -165,7 +176,7 @@ export class StoreRouterConnectingModule {
165176
(<any>this.router).hooks.beforePreactivation = (
166177
routerState: RouterStateSnapshot
167178
) => {
168-
this.routerState = routerState;
179+
this.routerState = this.serializer.serialize(routerState);
169180
if (this.shouldDispatchRouterNavigation())
170181
this.dispatchRouterNavigation();
171182
return of(true);
@@ -214,7 +225,12 @@ export class StoreRouterConnectingModule {
214225
private dispatchRouterNavigation(): void {
215226
this.dispatchRouterAction(ROUTER_NAVIGATION, {
216227
routerState: this.routerState,
217-
event: this.lastRoutesRecognized,
228+
event: {
229+
id: this.lastRoutesRecognized.id,
230+
url: this.lastRoutesRecognized.url,
231+
urlAfterRedirects: this.lastRoutesRecognized.urlAfterRedirects,
232+
state: this.serializer.serialize(this.routerState),
233+
} as RoutesRecognized,
218234
});
219235
}
220236

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { InjectionToken } from '@angular/core';
2+
import { RouterStateSnapshot } from '@angular/router';
3+
4+
export abstract class RouterStateSerializer<T> {
5+
abstract serialize(routerState: RouterStateSnapshot): T;
6+
}
7+
8+
export class DefaultRouterStateSerializer
9+
implements RouterStateSerializer<RouterStateSnapshot> {
10+
serialize(routerState: RouterStateSnapshot) {
11+
return routerState;
12+
}
13+
}

0 commit comments

Comments
 (0)