Angulars RouteReuseStrategy

Angular Jan 02, 2021
The strategy to give us the power to decide which route component is allowed to live beyond its routing, and which route component is doomed to be created and recreated everytime a user leaves or enters its route.

Well, it sounds dramatic, but a) it is the truth, and b) it seems to be one of the most underrated features of Angular, so let's spread some RouteReuseStrategy love! <3

The Problem

We got some big ol' third party library doing fancy data stuff, so we initialize it and use it to fetch some data. First initialization takes a bit, second fetching data also takes a bit.

No problem, outsource the initialization part to a service so that we only need to do it once, second... cache the requests so that we do them only once - Problem solved? Yes, kind of.

Navigating back and forth that component with eagle eyes locked to the screen still gives away something: There is a small amount of time, even though we might have everything optimized to the max, in which the component is still "loading", the table to display the data might be initially empty, or the loading spinner pops up only for a blink of an eye even though everything is cached.

This time is the time Angular takes to recreate the component, call our beloved ngOnInit method and kick things off.

However, this phenomena can only be seen when navigating between routes referencing different component classes.

/users/123 -> /users -> /users/321
			    vs.
/users/123      ->      /users/321

In the first example, the user detail component would not be reused, in the second example it would be reused since only the parameters changed.

The RouteReuseStrategy

To mitigate or extend this behaviour, we can leverage the RouteReuseStrategy the @angular/router package provides us with.

Implementing this abstract class allows us to distinctivly decide which route component should be "saved" for later or which should be destroyed after navigating away from it.

Let's check it out, shall we?

/**
 * @description
 *
 * Provides a way to customize when activated routes get reused.
 *
 * @publicApi
 */
export 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.
   *
   * Storing a `null` value should erase the previously stored value.
   */
  abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle|null): 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|null;

  /** Determines if a route should be reused */
  abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
}
Angulars RouteReuseStrategy Abstract Class

What we can see here are 4 essential methods, namely:

  • shouldReuseRoute lets us decide if we want to allow Angular to reuse the same component object when navigating between routes referencing the same component class.
  • shouldDetach and store allows us to decide whenever an component object should be stored, and also how/where it should be stored.
  • shouldAttach and retrieve allows us to decide whenever an component object should be retrieved and reattached.

To further foster our knowledge about how to use it, let's also look at Angulars default implementation of it.

/**
 * @description
 *
 * This base route reuse strategy only reuses routes when the matched router configs are
 * identical. This prevents components from being destroyed and recreated
 * when just the fragment or query parameters change
 * (that is, the existing component is _reused_).
 *
 * This strategy does not store any routes for later reuse.
 *
 * Angular uses this strategy by default.
 *
 *
 * It can be used as a base class for custom route reuse strategies, i.e. you can create your own
 * class that extends the `BaseRouteReuseStrategy` one.
 * @publicApi
 */
export abstract class BaseRouteReuseStrategy implements RouteReuseStrategy {
  /**
   * Whether the given route should detach for later reuse.
   * Always returns false for `BaseRouteReuseStrategy`.
   * */
  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return false;
  }

  /**
   * A no-op; the route is never stored since this strategy never detaches routes for later re-use.
   */
  store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {}

  /** Returns `false`, meaning the route (and its subtree) is never reattached */
  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return false;
  }

  /** Returns `null` because this strategy does not store routes for later re-use. */
  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle|null {
    return null;
  }

  /**
   * Determines if a route should be reused.
   * This strategy returns `true` when the future route config and current route config are
   * identical.
   */
  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
  }
}

Ah, there we can see it already, and it's even written in the docstring of the implementation. So in our case, we are actually only interested in the last method shouldReuseRoute since we want to extend the behaviour of the above mentioned navigation from one component to another, referencing the same component class.

Creating a custom RouteReuseStrategy

Having gathered enough knowledge about the functionality of the RouteReuseStrategy from the Angular code itself, it's time to create our custom strategy.

import {ActivatedRouteSnapshot, DetachedRouteHandle, BaseRouteReuseStrategy} from '@angular/router';

export class AppRouteReuseStrategy implements BaseRouteReuseStrategy {
  public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return (future.routeConfig === curr.routeConfig) || future.data.reuseComponent;
  }
}

It's short, we basically only extended it via future.data.reuseComponent which allows us to declarativly decide for each route if we would like to reuse it or not.

But first we let angular know that we'd like to override its default configuration.

// ...
import {AppRouteReuseStrategy} from './app-route-reuse-strategy';
import {RouteReuseStrategy} from '@angular/router';


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

Once we have our module setup like above, we only thing missing is to tell angular which routes we would like to reuse via our route configuration:

const routes: Routes = [
  {
    path: 'app',
    component: MyComponent,
    data: {
      reuseComponent: true
    }
  },
  // ....
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {
}

And boom, now angular is reusing our route whenever we navigate away from it and come back to it afterwards. Very nice!

Congratulations!

Hey, very cool! I hope your projects works as expected now, you might have had something different in mind when you came here, so maybe it's worth a try playing around with the other methods too to manually store and retrieve component objects.

Tags

Nico Filzmoser

Let's share some cody bits.