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:
shouldDetach
, store
, shouldAttach
, retrieve
, and shouldReuseRoute
.app.module
file of your application.BaseRouteReuseStrategy
classAngular uses a default implementation of RouteReuseStrategy which does not cache any route. For more information about this class, checkout the documentation.
shouldAttach
methodAngular 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; }
shouldReuseRoute
methodAngular 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; }
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 {}
store
methodAngular 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; } }
retrieve
methodWhen 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; }
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.
shouldReuseRoute
returns false because the two routes are different.shouldAttach
returns false, it knows that it has to create the route from scratch.shouldDetach
returns true because I have set the reuseComponent flag to true for the component.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]
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.
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
Using MSAL angular to authenticate a user against azure ADB2C via authorization code flow with Proof Key for Code Exchange.
Displaying text in a tool tip when text overflows its container
Azure AD authentication in angular using MSAL angular v2 library
Common frameworks, libraries and design patterns I use
Rendering a PDF in angular that works with mobile browsers using ng2-pdf-viewer
Differences between a Promise and an Observable
Integrate Azure AD B2C reset password user flow in angular using oidc-client-js.
Integrate Azure AD B2C profile editing user flow in angular using oidc-client-js.