Quote of the Day

more Quotes

Categories

Buy me a coffee

Cache angular components using RouteReuseStrategy

Published April 8, 2023 in Angular - 0 Comments

The project I have worked on has several pages that load data in Angular Material tables. The application’s user interface consists of a left menu with multiple tabs, and clicking on each tab loads the corresponding component and its child on the right. However, initializing the component appears to be an expensive operation, as it takes time to load the parent and child components and initialize the Material table, especially when there is a considerable amount of data to be displayed.

This delay causes the application to become unresponsive, especially when the user switches between tabs quickly, causing the components to pile up. I initially thought that the issue was related to fetching data through the network, but caching the data did not help to improve the performance.

After researching the topic of reusing components, I discovered the RouteReuseStrategy class. This class provides hooks for developers to advise Angular on when to reuse a route and how to cache a route. By utilizing this class, we can avoid the expensive process of destroying and initializing the components and displaying data in the table.

A typical solution for caching components in an Angular application involves the following steps:

  • Identify the components that you want to cache. You can do this when defining the routes in your application.
  • Create a class that implements the RouteReuseStrategy and override the five methods required by the abstract class. These methods are shouldDetach, store, shouldAttach, retrieve, and shouldReuseRoute.
  • Register your custom RouteReuseStrategy in the app.module file of your application.

The default BaseRouteReuseStrategy class

Angular uses a default implementation of RouteReuseStrategy which does not cache any route. For more information about this class, checkout the documentation.

Implement the shouldAttach method

Angular calls this method and passes a route as the parameter to determine whether it should retrieve the route from the cache or create a new one. If shouldAttach returns true, Angular will retrieve the cached route by calling the retrieve method. If shouldAttach returns false, it will create a new route from scratch. In my code, I use a dictionary-like data structure to store cached routes. In the shouldAttach method, I simply check if the route is present in the cache by comparing its route path, and return true if it is present, or false otherwise.

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    const shouldAttach = !!route.routeConfig?.path && !!this.cachedRoutes[route.routeConfig.path];
    console.log(`AppRouteReuseStrategy#shouldAttach(${route.routeConfig?.path}) called. Return: ${shouldAttach}`);
    return shouldAttach; 
  }

Implement the shouldReuseRoute method

Angular calls the shouldReuseRoute method to determine whether to reuse the current route or not.

Initially, I was under the impression that this method needs to return true for caching the component to work. I modified the method to return true when the future component has the reuseComponent flag, as shown in the code snippet below.

shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    // this does not work. 
  return future.routeConfig === curr.routeConfig || future.route?.data?.ReuseComponent === true;
    console.log(`AppRouteReuseStrategy#shouldReuseRoute(future:${future.routeConfig?.path}, current: ${curr.routeConfig?.path}) called. Return: ${shouldReuseRoute}`);
    return shouldReuseRoute;
    }

However, upon testing, I observed that when navigating from one component to another, if shouldReuseRoute returns true, then angular does not navigate away from the page and nothing changes. After further investigation, I realized that the shouldReuseRoute method is not directly related to component caching, but rather determines whether or not to reuse the current route.

Below is the code that works for me. Essentially, it has the same behavior as Angular’s default route reuse strategy, which only reuses the route if the current and future route configurations are the same.

 shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    const shouldReuseRoute = future.routeConfig === curr.routeConfig;
    console.log(`AppRouteReuseStrategy#shouldReuseRoute(future:${future.routeConfig?.path}, current: ${curr.routeConfig?.path}) called. Return: ${shouldReuseRoute}`);
    return shouldReuseRoute;
    }

Implement the shouldDetach method

Angular calls the shouldDetach method to determine whether to cache the current route before navigating away from it. If this method returns true, Angular will then call the store method to cache the current route.

This is where the ReuseComponent flag comes into play. In my code, I check if the route contains the flag, and if so, return true to indicate to Angular that it should cache the route by calling the store method.

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    const shouldDetach = !!route.data[ReuseComponent] && !!route.component;
    console.log(`AppRouteReuseStrategy#shouldDetach(${route.routeConfig?.path}) called. Return: ${shouldDetach}`);
    return shouldDetach; 
  }

I can set the flag when defining the routes, as shown in the code snippet below.

const routes: Routes = [
  {
    path: BudgetUrlConstants.OtherSalaryBenefits,
    component: OtherSalaryBenefitsComponent,
    data: { reuseComponent: true }
  },
  {
    path: BudgetUrlConstants.OtherServiceSupplies,
    component: OtherServiceSuppliesComponent,
    data: { reuseComponent: true }
  },
  // other codes omitted for brevity 
];

@NgModule({
  imports: [
    // codes omitted for brevity 
  ],
  declarations: [
    OtherSalaryBenefitsComponent,
    OtherServiceSuppliesComponent,
   // codes omitted for brevity 
  ],
})
export class FeatureUserModule {}

Implement the store method

Angular calls the store method to cache the current route before navigating away from it. How to store the route is up to the developer.

In my code, I store the route in a dictionary-like data structure where the key is the route path and the value is the instance of DetachRouteHandle. This is because Angular expects this type when it calls the retrieve method to get the cached route later.

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
    console.log(`AppRouteReuseStrategy#store(${route.routeConfig?.path}) called.`);
    if (route.routeConfig?.path && handle) {
      console.log(`Caching route: ${route.routeConfig?.path}`);
      this.cachedRoutes[route.routeConfig.path] = handle; 
    }
  }

Implement the retrieve method

When shouldAttach returns true, Angular calls the retrieve method to get the route from the cache instead of creating it from scratch.

In my code, I retrieve the route from the dictionary as shown in the code snippet below:

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
    let cachedRoute = null; 
    if (route.routeConfig?.path) {
      cachedRoute = this.cachedRoutes[route.routeConfig.path];
    }
    console.log(`AppRouteReuseStrategy#retrieve(${route.routeConfig?.path}) called. Return: ${cachedRoute}`)
    return cachedRoute; 
  }

Putting it together

Here is the complete class that implements RouteReuseStrategy:

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

type CachedRoute = {
  [key: string | symbol]: any;
};

const ReuseComponent: string = 'reuseComponent';


export class AppRouteReuseStrategy implements RouteReuseStrategy {


  private cachedRoutes: CachedRoute = {};

  shouldDetach(route: ActivatedRouteSnapshot): boolean {
    const shouldDetach = !!route.data[ReuseComponent] && !!route.component;
    console.log(`AppRouteReuseStrategy#shouldDetach(${route.routeConfig?.path}) called. Return: ${shouldDetach}`);
    return shouldDetach; 
  }

  store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
    console.log(`AppRouteReuseStrategy#store(${route.routeConfig?.path}) called.`);
    if (route.routeConfig?.path && handle) {
      console.log(`Caching route: ${route.routeConfig?.path}`);
      this.cachedRoutes[route.routeConfig.path] = handle; 
    }
  }

  shouldAttach(route: ActivatedRouteSnapshot): boolean {
    const shouldAttach = !!route.routeConfig?.path && !!this.cachedRoutes[route.routeConfig.path];
    console.log(`AppRouteReuseStrategy#shouldAttach(${route.routeConfig?.path}) called. Return: ${shouldAttach}`);
    return shouldAttach; 
  }

  retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
    let cachedRoute = null; 
    if (route.routeConfig?.path) {
      cachedRoute = this.cachedRoutes[route.routeConfig.path];
    }
    console.log(`AppRouteReuseStrategy#retrieve(${route.routeConfig?.path}) called. Return: ${cachedRoute}`)
    return cachedRoute; 
  }

  shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    const shouldReuseRoute = future.routeConfig === curr.routeConfig;
    console.log(`AppRouteReuseStrategy#shouldReuseRoute(future:${future.routeConfig?.path}, current: ${curr.routeConfig?.path}) called. Return: ${shouldReuseRoute}`);
    return shouldReuseRoute;
    }
}

Below is the relevant log output that provides hints about the order in which Angular calls the methods when the user navigates from one route to another.

// switching from route A to route B
AppRouteReuseStrategy#shouldReuseRoute(future:serviceSupplies, current: salaryBenefits) called. Return: false
app-route-reuse-strategy.ts:32 AppRouteReuseStrategy#shouldAttach(serviceSupplies) called. Return: false
app-route-reuse-strategy.ts:17 AppRouteReuseStrategy#shouldDetach(salaryBenefits) called. Return: true
app-route-reuse-strategy.ts:22 AppRouteReuseStrategy#store(salaryBenefits) called.
  • As indicated in the logs, when switching from route A (salaryBenefits) to route B (serviceSupplies), shouldReuseRoute returns false because the two routes are different.
  • Angular then checks to see if we have a cached route for route B. Because shouldAttach returns false, it knows that it has to create the route from scratch.
  • Before navigating away from route A, however, Angular calls the shouldDetach() method to see if it should cache route A. In this case, shouldDetach returns true because I have set the reuseComponent flag to true for the component.
  • To cache the route A, Angular in turn calls the store method.

Below is the relevant log output that shows what happens when the user switches from route B (serviceSupplies) back to route A (salaryBenefits).

// switching from route B back to route A
AppRouteReuseStrategy#shouldReuseRoute(future:salaryBenefits, current: serviceSupplies) called. Return: false
app-route-reuse-strategy.ts:32 AppRouteReuseStrategy#shouldAttach(salaryBenefits) called. Return: true
app-route-reuse-strategy.ts:41 AppRouteReuseStrategy#retrieve(salaryBenefits) called. Return: [object Object]
app-route-reuse-strategy.ts:17 AppRouteReuseStrategy#shouldDetach(serviceSupplies) called. Return: true
app-route-reuse-strategy.ts:22 AppRouteReuseStrategy#store(serviceSupplies) called.
app-route-reuse-strategy.ts:32 AppRouteReuseStrategy#shouldAttach(salaryBenefits) called. Return: true
app-route-reuse-strategy.ts:41 AppRouteReuseStrategy#retrieve(salaryBenefits) called. Return: [object Object]
  • As before, since the two routes are different, shouldReuseRoute() returns false.
  • Angular then calls shouldAttach() method, passing the future route (route A) to see if we have a cached version of the route. In this case, since we have previously cached route A, shouldAttach() returns true.
  • Angular then calls retrieve() method, passing route A as a parameter to retrieve the route from the cache.

That’s it. After I get the caching to work, the app is much more responsive. Now, if the user comes back to a page that is cached, angular displays the page super quick because it does not need to render the component anymore.

References

Angular – BaseRouteReuseStrategy

Build a Route Reuse Strategy with Angular | Bits and Pieces (bitsrc.io)

How to Toggle Caching for Routing Components in Angular | by Luka Onikadze | The Startup | Medium

No comments yet