How to reuse rendered component in Angular 2.3+ with RouteReuseStrategy

Pavel Gerasimov
5 min readApr 30, 2017

--

Sometimes we face an interesting problem in single-page application: we need our parent component to be reused after navigating back to it from any child route. Some examples:

  • a list should pick up previous scroll position and selection,
  • a map should restore its position, zoom and added objects after going to object details,
  • a video player should restore playback position, added markers, comments,
  • a chart should restore its zoom, colors, scroll positions, data sets, etc
ag-grid restored its scroll position and selection after navigating back from details view

What we want

Other words, we want the parent view to be as if it is ‘hided’ and the child view as if it is shown over the parent

Preseve parent view from reinitialization with hiding it under the child view

This way parent view will not be changed because it stays untouched in the DOM. But it looks really crazy to use this approach with Angular routing

Other crazy approach is to store everything you can access in the local storage and manual restore each time for each component. It works but it takes so much time for you — may be Angular has a better way to resolve it?

Angular routing basics

At first, lets take a look to Angular routing itself. The important idea here is that Angular creates a RouteStateSnapshot every time when redirect is applied after navigation. RouteStateSnapshot is an immutable object that stores ActivatedRouteSnapshots tree. When we navigate to the new page or just change a URL parameter we get new RouteStateSnapshot

interface RouterStateSnapshot {
root: ActivatedRouteSnapshot;
}

interface ActivatedRouteSnapshot {
url: UrlSegment[];
params: {[name:string]:string};
data: {[name:string]:any};

queryParams: {[name:string]:string};
fragment: string;

root: ActivatedRouteSnapshot;
parent: ActivatedRouteSnapshot;
firstchild: ActivatedRouteSnapshot;
children: ActivatedRouteSnapshot[];
}

ActivatedRouteSnapshot represents the state of our route (and all of its components) at a moment in time. So, could we reuse these snapshots?

Angular cares for you

Angular provides its own approach to solve these tasks since 2.3. RouteReuseStrategy is a native way to to customize when Angular should reuse route snapshots

We can not just say “Yes, please reuse all my routes”. To use the strategy in our application we need to extend the following absctract class:

export declare abstract class RouteReuseStrategy {
/**
* Determines if this route (and its subtree) should be detached to be reused later.
*/
abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
/**
* Stores the detached route.
*/
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void;
/**
* Determines if this route (and its subtree) should be reattached.
*/
abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
/**
* Retrieves the previously stored route.
*/
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle;
/**
* Determines if a route should be reused.
*/
abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
}

There are 3 main tasks that we need to solve here:

  • storing our snapshots somewhere
  • deciede whether we should reuse a route
  • deciede whether we should clear the routes cache

Storing routes snapshots

Let’s create a field handlers in our CustomReuseStrategy implementation

export class CustomReuseStrategy implements RouteReuseStrategy {

handlers: {[key: string]: DetachedRouteHandle} = {};

shouldDetach(route: ActivatedRouteSnapshot): boolean {
return true; // reuse all routes
}

store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
this.handlers[route.routeConfig.path] = handle;
}
...
}

This way all snapshots will be stored in handlers using the path from the config as a key. store will be called just if shouldDetach returns true, so you can customize storing logic here.

At practive, using route.routeConfig.path is not a great idea when your path contains params. For example, with route /list/:id all items with urls /list/1, /list/2 and /list/3 will be stored with a key /list/:id

To retrieve actual path we should useroute.url.join("/") that will return all path parts from the root of our module

What about lazy loading modules? If we use route.url.join("/") here we will just get a "" for the module root. Let’s come back to ActivatedRouteSnapshot definition — it is a tree and it has parent field, so we can access route.parent.url.join("/"). Other words, you need to implement a method which will go up through the tree, join all modules paths together and return a string like that you see in the browser address bar. Or, if you have simple application without deep modules structure use route.url.join("/") || route.parent.url.join("/")

Advanced shouldAttach implementation

Here we decide if we should attach stored handler or not. This method does not return handler itself and returns boolean. If true, retrieve will be called

retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle

and will return the original handler. The simple implementation for shouldAttach is here:

shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!this.handlers[url];
}

We just check if we have stored handler for this route or not. Advanced cases are:

  • logout — new user should not get cached views from the old one
  • language switching — if we have set a new language, it is a bad idea to show cached views with old one
  • layout changes — if we support layout customization and will change it for the several routes, we will see same layout for each route as we saw the first time for it
  • preserved routes — you may want to preserve some routes from being stored (for example, details views that need actual data from the server)

How to deal with it?

Clear handlers on logout:

shouldAttach(route: ActivatedRouteSnapshot): boolean {
if (route.component == LogoutComponent) {
this.handlers = {};
return false;
}
}

Clear handlers on route switch:

private currentLanguage: string;

constructor(@Inject(SessionStorageService) private storage: SessionStorageService) {
this.currentLanguage = this.storage.retrieve('language');
}
....
shouldAttach(route: ActivatedRouteSnapshot): boolean {
let newLang = this.storage.retrieve('language');
if (newLang != this.currentLanguage) {
this.currentLanguage = newLang;
this.handlers = {};
return false;
}
}

Prevent restore for the preserved routes:

private preservedUrls = [
/details/,
/actial-data/
]
....
shouldAttach(route: ActivatedRouteSnapshot): boolean {
let url = route.url.join("/") || route.parent.url.join("/");
for (let preservedUrl of this.preservedUrls) {
if (preservedUrl.test(url)) {
return false;
}
}
}

We may combine these cases and build our own customized strategy

Complete code example

For those who waits for a snippet:

export class CustomReuseStrategy implements RouteReuseStrategy { 

private handlers: {[key: string]: DetachedRouteHandle} = {};


constructor() {

}

shouldDetach(route: ActivatedRouteSnapshot): boolean {
return true;
}

store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
this.handlers[route.url.join("/") || route.parent.url.join("/")] = handle;
}

shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!this.handlers[url];
}

retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
return this.handlers[route.url.join("/") || route.parent.url.join("/")];
}

shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}

}

How to use RouteReuseStrategy in Angular app

It is pretty simple:

@NgModule({
[...],
providers: [
{provide: RouteReuseStrategy, useClass: CustomReuseStrategy}
]
)}
export class AppModule {
}

That’s it!

What else?

RouteReuseStrategy appeared in Angular 2.3 instead of deprecated canReuse and become a flexible tool for customizing router behaviuor. It is useful for charts, maps, players, grids and other elements with its own complicated structure and behaviour

What cases do you use it in your projects?

--

--

Pavel Gerasimov

Senior Engineering Manager at Wrike. Growth Engineering, Org and Leadership Transformation. Former CEO and co-founder of Le Talo Robotics