circuit

Angular Route Reuse Strategy




When navigating away from a route, Angular destroys the corresponding component, except if the new route uses the same one (if we navigate from /user/12 to /user/57 for example). Angular reuses components when it can but destroys them to free memory otherwise and this is the desired behavior in most cases.

    /user/12    ---------------------------------->   /user/57
  UserComponent                                     UserComponent


   /user/12     -------------> /users ------------>   /user/57
  UserComponent           UserListComponent         UserComponent

In the first case, the UserComponent would be reused. In the second one though, the UserComponent would be destroyed when navigating to /users and would be recreated when navigating to /user/57.

Destroying a component when you know that you will need it again and soon, is something you want to avoid. As always, Angular provides a solution and lets you define your own reuse strategy by implemeting the RouteReuseStrategy class.

Master/Detail Interface

A classic example of a case in which you could profit from implementing your own reuse strategy would be a master/detail interface. In such an interface, the user usually switches between the master view (the list of the elements) and details views. Destroying/rebuilding theses pages after each navigation is not optimal.

You can see if the gif below that there is a small delay before the list appears every time we close the details page. It is because the list component is fetching the books every time it gets built. We could prevent this by caching and reusing the list component.

Custom Route Reuse Strategy

To prevent the components from being destroyed when we navigate between the views, we need to implement our own route reuse strategy. We create a class CustomRouteReuseStrategy implementing the abstract class RouteReuseStrategy and provide it in our module.

app.module.ts

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { BookListComponent } from "./book-list/book-list.component";
import { BookDetailsComponent } from "./book-details/book-details.component";
import { HttpClientModule } from "@angular/common/http";
import { RouteReuseStrategy } from "@angular/router";
import { CustomRouteReuseStrategy } from "./custom-route-reuse-strategy";

@NgModule({
  declarations: [AppComponent, BookListComponent, BookDetailsComponent],
  imports: [BrowserModule, HttpClientModule, AppRoutingModule],
  providers: [
    {
      provide: RouteReuseStrategy,
      useClass: CustomRouteReuseStrategy,
    },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

custom-route-reuse-strategy.ts

import {
  RouteReuseStrategy,
  ActivatedRouteSnapshot,
  DetachedRouteHandle,
} from "@angular/router";

export class CustomRouteReuseStrategy implements RouteReuseStrategy {
  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    throw new Error("Method not implemented.");
  }
  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    throw new Error("Method not implemented.");
  }
  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    throw new Error("Method not implemented.");
  }
  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    throw new Error("Method not implemented.");
  }
  shouldReuseRoute(
    future: ActivatedRouteSnapshot,
    curr: ActivatedRouteSnapshot
  ): boolean {
    throw new Error("Method not implemented.");
  }
}

As you can see, to implement the RouteReuseStrategy class we have to implement five methods (with their definitions from the Angular Documentation):

  • shouldDetach: Determines if this route (and its subtree) should be detached to be reused later.

  • store: Stores the detached route.

  • shouldAttach: Determines if this route (and its subtree) should be reattached.

  • retrieve: Retrieves the previously stored route.

  • shouldReuseRoute: Determines if a route should be reused.

The default route reuse strategy from Angular looks like this:

default-reuse-strategy.ts

export class DefaultRouteReuseStrategy implements RouteReuseStrategy {
  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return false;
  }
  store(
    route: ActivatedRouteSnapshot,
    detachedTree: DetachedRouteHandle
  ): void {}
  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return false;
  }
  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
    return null;
  }
  shouldReuseRoute(
    future: ActivatedRouteSnapshot,
    curr: ActivatedRouteSnapshot
  ): boolean {
    return future.routeConfig === curr.routeConfig;
  }
}

As we previously said, Angular reuses the route if the new route has the same config (uses the same component) as the previous one, otherwise it doesn’t. We want to keep this behaviour so we copy this implementation of the shouldReuseRoute function. This function would be the right place to change if you wanted to prevent Angular from reusing components when navigating from /user/57 to /user/12 but this is not what we are intending to do.

We are interested in the other four methods. shouldDetach and store are called when you leave a route. If shouldDetach returns true, then we need to store the route as we want to reuse later. shouldAttach and retrieve are called when entering a route. If shouldAttach returns true, we want to reuse a previously stored route instead of building new components.

Store and Retrieve the List Component

To cache the list component, we first need to store it. We should therefore return true to shouldDetach when the route path is equal to the list view’s path, in our case list. We need to take care of the storing ourselves, but it is easier as it sounds. We only need to store the DetachedRouteHandle corresponding to our route, for example in a Map using the route paths as keys.

custom-reuse-strategy.ts

export class CustomRouteReuseStrategy implements RouteReuseStrategy {
  private storedRoutes = new Map<string, DetachedRouteHandle>();

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return route.routeConfig.path === 'list';
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    this.storedRoutes.set(route.routeConfig.path, handle);
  }

  [...]
}

That's it for the storing. Now, when coming back to the list view, we want to reuse our stored route handle, if we have one. Therefore we return true to shouldAttach if there is a value in our storedRoutes for given route path. Retrieving the route handle is easy, we return the value from storedRoutes corresponding to the path.

custom-route-reuse-strategy.ts

import {
  RouteReuseStrategy,
  ActivatedRouteSnapshot,
  DetachedRouteHandle,
} from "@angular/router";

export class CustomRouteReuseStrategy implements RouteReuseStrategy {
  private storedRoutes = new Map<string, DetachedRouteHandle>();

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return route.routeConfig.path === "list";
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    this.storedRoutes.set(route.routeConfig.path, handle);
  }

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    return (
      !!route.routeConfig && !!this.storedRoutes.get(route.routeConfig.path)
    );
  }

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    return this.storedRoutes.get(route.routeConfig.path);
  }

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

The list component is now stored and reused, avoiding the books to have to be fetched every time we enter the master view. We don’t see this short delay anymore.

This was a simple example but I hope it helped you see how to implement a custom reuse strategy. The five methods might look a bit confusing at first, but they actually have well defined roles:

  • shouldReuseRoute lets you decide if you want to allow Angular to reuse the same component object when navigating between routes referencing the same component class or if Angular should destroy and rebuild the component every time.

  • shouldDetach and store respectively lets you decide if a component should be stored for later and if yes lets you store it. It is called when leaving a route.

  • shouldAttach and retrieve respectively lets you decide if a stored component should be reused instead of creating a new component, and if yes lets you give this stored component to Angular. It is called when entering a route.

Sources:

The example was built using OpenLibrary. Books can be fetched from their ISBN, so to fetch the three Lord of the Rings books as a JSON object we called:

[https://openlibrary.org/api/books?format=json&jscmd=data&bibkeys=ISBN:0261103253,ISBN:9780261102361,ISBN:9780618002245](https://openlibrary.org/api/books?format=json&jscmd=data&bibkeys=ISBN:0261103253,ISBN:9780261102361,ISBN:9780618002245)

Further Reading

How to reuse React components across your projects




Continue Learning