RouteReuseStrategy: advantages and pitfalls

The Angular philosophy is to destroy a Component as soon as it’s not in use anymore, meaning: not in the DOM anymore, and this happens quickly when RouterOutlet is used to display such or such content, depending on the URL route. The Component will be created again, if the user navigates back to a route that contains it.

Recreating Components can be trouble (depending on the usecase)

This is useful for large applications (to reduce the memory) where Components are kept simple or when they will not be met again often enough to care about their destruction/reconstruction time.

But for applications where the user is expected to go back and forth the same Components, this can create several uncomfortable side-effects for the user experience:

  • a clipping may result, especially if some somewhat heavy initialization process occurs in the Component (= in ngOnInit); though, this might reveal a design smell. For instance, all data fetching (typically, from the API server) and calculation should be done and kept in a sidecar service, whose responsability will be to handle the ‘data state’ related to the Component, which can fetch it from there very quickly when initializing again.
  • the ‘UI state’ of the Component is lost, and that might go against the user experience. By ‘UI state’ (versus ‘data state’), I mean all the little interface details, not related to any business data, that evolves when the user plays with the Component. Examples: a selection in a Select node; the beginning of some text in a TextArea node; the expanding of a collapsable widget; the scrolling in a large list; the navigation in a map. For form-related elements, it would be possible to store the Form state in a sidecar, but for the rest of the myriads of possible little things, it wouldn’t be possible (too much effort) to store them all and restore them when the Component is met again, and the user would face a ‘default UI state’ each time. Again, this depends on the complexity and nature of the Components, and if we care at all about the UI to reset everytime.
  • when depending on third-party Component, we may still face an initialization time, resulting in clippings and annoying waitings. It will most likely be very difficult to save the internal ‘UI state’ of such Components, and for some of them, it would need some unwanted hacking. A third-party map component, for instance, is likely to request its tiles again if destroyed and recreated.

Angular RouteReuseStrategy to the rescue!

Angular offers a mechanism to keep a Component on the side when navigating away and use it again – unchanged – when the same route is met again, called RouteReuseStrategy. The set up is not sexy; it feels more like a toolbox than an integrated part of the framework, but it’s not difficult, and soon enough you can specify the different routes of your applications, whose related Components will be preserved for later (note that you define reuseable routes; you can’t flag a Component itself as reusable). And that’s it! The navigation is lightning flash again, your Components are met again as they were, hooray!

Notice: be wary though that your Component, when kept aside for later, is still very much alive. Its subscription will still be active, so be sure that your Component is not subscribed to anything exterior to itself (which should be very much the case anyway!).

RouteReuseStrategy: caveats

Sadly, no solution is ever perfect straight away.

In my design, there’s a little piece missing from the RouteReuseStrategy (SO users still agrees that it feels more like a bug), which is: some Component lifecycle hooks, to be alerted of its detaching (when it’s kept aside for later) and reattaching (reused).

Again, it might not be a problem for a lot of usecases. For mine, I couldn’t think about a proper alternative to what I have.

Description:

  • My Component depends on a part of the URL to identify which entity to display. For instance, the route /station/14 is linked to StationComponent (via RouterOutlet) and indicates the Station #14 should be displayed within it.
  • The Component should initialize depending on the route (and not some external state), because we want the same behavior whether the user has come to this route by navigating the application, or by launching this URL directly. For that purpose, subscribing to ActivatedRoute.params seems the most logical way, as this Observable emits some ParamMap, in which the interesting bit of the URL (defined in the routes given to the RouterModule) is directly proposed as paramMap.get(‘stationId’). Awesome!
  • Some other Components in my application rely on the displayed Station (if any). These Components can not subscribe to ActivatedRoute.params as they’re not connected to the route (from the RouterModule point of view). They could listen to URL changes by other means, but in a messy way, most likely very coupled to the route definitions. To resolve this, simple: we use a state service to keep the information of the displayed Station, on which my other Components can subscribe, and which is modified by my StationComponent whenever it detects a change in the route parameters.
  • Now if the user navigates to another Component, say, a Digest, then my state service will be alerted that now a Digest is displayed, and not a Station anymore. If he navigates back to a Station – one different than the first – then StationComponent is reused, it detects the change in the route parameters and notifies the state service that now a Station is displayed, this very new Station. Good!
  • BUT! If the user navigates away and come back to the same Station, then StationComponent is reused, but this time it does not see any changes in the route parameters (if it was 14, from /station/14 before, and now it’s still /station/14, then no changes have occured, as the observed ActivatedRoute is only the one related to StationComponent). No event is emitted by ActivatedRoute.params, and the notifying of the state service is not done, resulting in a unsynced state within my application. Curses !

That’s where I lack of an elegant way to resolve this. To me, the Component should be able to react to its reattaching, during which it would have an extra opportunity to notify the state service.

But RouteReuseStrategy does not call any Component lifecycle hooks. : /

RouteReuseStrategy’s missing Component Lifecyle hooks: a fix

A workaround (found on SO) consists on replacing the default RouterOutlet by a custom one, which will trigger some lifecycle hooks in its related Component.

sv-router-outlet.directive.ts

import { ComponentRef, Directive } from '@angular/core';
import { ActivatedRoute, RouterOutlet } from '@angular/router';

@Directive({
  selector: 'sv-router-outlet',
})
export class SvRouterOutletDirective extends RouterOutlet {

  detach(): ComponentRef<any> {
    const instance: any = this.component;
    if (instance && typeof instance.onDetach === 'function') {
      instance.onDetach();
    }
    return super.detach();
  }

  attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute): void {
    super.attach(ref, activatedRoute);
    if (ref.instance && typeof ref.instance.onAttach === 'function') {
      ref.instance.onAttach(ref, activatedRoute);
    }
  }
}

To use, define a Module for this new outlet, and load the module wherever you’ll replace the default <router-outlet> by <sv-router-outlet>.

sv-router-outlet.module.ts

import { NgModule } from '@angular/core';
import { SvRouterOutletDirective } from './sv-router-outlet.directive';

@NgModule({
  declarations: [
    SvRouterOutletDirective,
  ],
  exports: [
    SvRouterOutletDirective,
  ],
})
export class SvRouterOutletModule { }

Leave a Reply

Your email address will not be published. Required fields are marked *