Angular2+ routeReuseStrategy lifecycle hooks

Viewed 1917

I'm trying to take advantage of a custom RouteReuseStrategy and for that I'd like to suspend any subscriptions when a component is detached and other things like scrolling to the proper position.

I've checked for possible hooks, but apparently no additional lifecycle hooks have been added and OnDestroy isn't called.

I tried to add my own onDetach and onAttach hooks, but neither the ActivatedRouteSnapshots nor the DetachedRouteHandle will give me the instance of the current component (just the prototype?).

The only way I could get a grip on the component instance when navigating away by using the CanDeactivate guard, but that doesn't seem right. And I still couldn't find a way to add a hook for onAttach.

So my question is, how can I properly suspend a component when detached and resume it when attached ?

There used to be a OnActivate hook-interface in @angular/router, but that seems to be gone and I didn't see any replacement.

P.S. there seem to some reports of Angular apps with custom RouteReuseStrategies slowing down when used for extended time, probably because there is no way to pause/resume components.

2 Answers

I fixed it (yes this is more a bug than a missing feature).

Extend RouterOutlet and override attach() and detach() (see below), then replace your <router-outlet> tags with <app-router-outlet>. If your component has a onDetach and/or onAttach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute) method, it will be called.

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

@Directive({
  selector: 'app-router-outlet',
})
export class AppRouterOutletDirective 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);
    }
  }
}

P.S. Even with that fix custom RouteReuseStrategies are still pretty useless, without access to history states or preserved data in the ActivatedRoute there is no way of identifying two instances with the same routeConfig/path.

Paired with some weird timing of angular's calls to RouteReuseStrategy, Router Navigation Events and history.pushState it's extremely hard to write workarounds for that.

This is extremely frustrating to work with.

I extended the answer from Su-An Hwang to not directly extend the router-outlet with the directive and just use an extra attribute to add the lifecycle hooks.

This should make adding the extra lifecycle hooks less invasive in the app.

@Directive({
  selector: '[appRouteReuseLifecycle]',
})
export class RouteReuseLifeCycleDirective {
  constructor(private routerOutlet: RouterOutlet) {
    // Methods need to be patched before content init
    this.patchMethods();
  }

  /**
   * This method patches both onDetach and onAttach methods to call the relevant
   * hooks in the child component before or after running the existing logic
   * @private
   */
  private patchMethods() {
    // Save the original attach method
    const originalAttach = this.routerOutlet.attach;
    // Override the component method
    this.routerOutlet.attach = (ref: ComponentRef<any>, activatedRoute: ActivatedRoute) => {
      originalAttach.bind(this.routerOutlet)(ref, activatedRoute);
      // Call the onAttach hook if exists
      const instance = this.routerOutlet.component as ComponentWithOptionalLifecycleHooks;
      if (instance && typeof instance.onAttach === 'function') {
        instance.onAttach();
      }
    };

    // Save the original detach method
    const originalDetach = this.routerOutlet.detach;
    this.routerOutlet.detach = () => {
      const instance = this.routerOutlet.component as ComponentWithOptionalLifecycleHooks;
      if (instance && typeof instance.onDetach === 'function') {
        instance.onDetach();
      }
      // return the detached component with the original method
      return originalDetach.bind(this.routerOutlet)();
    };
  }
}

interface ComponentWithOptionalLifecycleHooks {
  onAttach?: () => void;
  onDetach?: () => void;
}
Related