Boosting Angular App Performance with RouteReuseStrategy

Brajraj Agrawal
5 min readMar 23, 2024

--

In single-page applications (SPAs), navigation between views often involves creating and destroying components. This can be detrimental to performance, especially in complex applications with heavy components. The Angular RouteReuseStrategy provides a powerful mechanism to optimize navigation by reusing components when appropriate.

What is Router Reuse Strategy?

The Router Reuse Strategy is an interface in Angular that allows developers to customize the router’s behavior when it comes to reusing routes. By implementing this interface, you can control which routes are cached and under what conditions. This can significantly enhance the user experience by:

  • Improving perceived performance: Navigation feels smoother and faster as components don’t need to be recreated from scratch.
  • Reducing memory usage: By reusing components, you can lessen the application’s memory footprint, leading to a more stable experience.
  • Preserving component state: If a component holds user-entered data or performs expensive calculations, reusing it can maintain its state between navigations.

Real-Life Examples of Router Reuse Strategy

  • E-commerce Shopping Cart: When a user navigates between product categories, you can reuse the shopping cart component to preserve the selected items and quantity.
  • Form-Heavy Applications: In applications with lengthy forms, reusing the form component allows users to pick up where they left off without losing their progress.
  • Content Management Systems (CMS): CMS dashboards often involve complex views with data grids and filters. Reusing these components during navigation can streamline the editing experience.

Explanation of Router Reuse Strategy Methods:

store(snapshot: ActivatedRouteSnapshot): DetachedRouteHandle:

  • This method determines whether a route should be cached when the user navigates away from it.
  • It receives an ActivatedRouteSnapshot object containing information about the route being deactivated.
  • You can implement logic here to decide which routes to cache based on various factors:
  • Presence of a custom data property on the route configuration (e.g., data: { store: true }).
  • Comparison of route parameters (e.g., if specific route parameters change, you might not want to reuse).
  • If the route should be cached, return a DetachedRouteHandle object with two properties:
  • handle: Represents the route's component factory, allowing the router to recreate the component later.
  • selector: The route's path or selector string used for retrieval.
  • Returning null indicates the route won't be cached.

shouldAttach(route: ActivatedRouteSnapshot): boolean:

  • This method is called when the user navigates to a new route.
  • It receives an ActivatedRouteSnapshot object for the incoming route.
  • Your logic here determines whether to reuse a previously cached component or create a new one.
  • Return true if a cached component can be attached (reused), and false to force a new instance.
  • You can base the decision on:
  • Availability of a cached handle from the store method.
  • Whether the route configuration has a data property indicating reusability (e.g., data: { reuse: true }).

shouldDetach(route: ActivatedRouteSnapshot): boolean:

  • This method is invoked before the current route is deactivated.
  • It receives an ActivatedRouteSnapshot object for the route being left behind.
  • It should only return true if you previously returned a non-null value from store(route), indicating the route is cached.
  • This ensures components are only detached if they’re intended to be reused.

retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle:

  • This method is called when the user navigates back to a previously cached route.
  • It receives an ActivatedRouteSnapshot object for the route attempting retrieval.
  • Implement logic here to retrieve the cached handle based on route information (e.g., route path).
  • You might use mechanisms like localStorage or a dedicated caching service:
  • For localStorage: Check if there's an entry with the route's path or selector.
  • For a dedicated service: Query the service with route information to fetch the handle.
  • If a cached handle is found, return it. Otherwise, return null to create a new component.

Scenarios and Considerations

Navigation Back:

  • When the user navigates back to a cached route, shouldAttach will be called with the cached route's information.
  • If you previously returned true from shouldDetach, indicating reusability, and a handle was successfully retrieved from retrieve, the router will reuse the cached component.
  • This leads to a smoother user experience, as the component’s state remains intact.

Creating a New Component (Bypassing Caching):

  • There are a few ways to prevent a component from being cached, even if it has reuse logic implemented:
  • Omit the data: { reuse: true } property in the route configuration for that specific route.
  • Implement logic within shouldDetach to return false for routes that shouldn't be cached under certain conditions (e.g., based on route parameters or query parameters).
  • If you need to force a new component instance even when caching is enabled, you can:
  • Clear the cached handle from storage (e.g., localStorage) before navigation.
  • Use a service to manage the cache and manually remove the entry for the desired route.

Implementation

  1. Create a Custom Reuse Strategy:
import { Injectable } from '@angular/core';
import { RouteReuseStrategy } from '@angular/router';

@Injectable()
export class CustomReuseStrategy implements RouteReuseStrategy {

store(snapshot: ActivatedRouteSnapshot): DetachedRouteHandle {
// Add logic to determine which routes to store (e.g., by route data)
if (snapshot.data['store']) {
return { handle: snapshot.componentFactory, selector: snapshot.routeConfig.path };
}
return null;
}

shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!route.routeConfig && !!route.data['reuse'];
}

shouldDetach(route: ActivatedRouteSnapshot): boolean {
return !!this.store(route); // Detach only if the route is stored
}

retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
// Retrieve the previously stored handle based on route information
const stored = localStorage.getItem(route.routeConfig.path);
if (stored) {
return JSON.parse(stored);
}
return null;
}
}

2. Provide the Custom Strategy in Your AppModule:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AppComponent } from './app.component';
import { CustomReuseStrategy } from './custom-reuse-strategy';

const routes: Routes = [
// ... your routes
];

@NgModule({
declarations: [AppComponent],
imports: [RouterModule.forRoot(routes)],
providers: [{ provide: RouteReuseStrategy, useClass: CustomReuseStrategy }],
bootstrap: [AppComponent]
})
export class AppModule { }

3. Mark Routes for Reuse (Optional):

const routes: Routes = [
{ path: 'products', component: ProductsComponent, data: { reuse: true } },
{ path: 'cart', component: CartComponent, data: { store: true } },
// ... other routes
];

Pros and Cons

Pros:

  • Improved perceived performance and reduced memory usage.
  • Preservation of component state between navigations.
  • Enhanced user experience for complex applications.

Cons:

  • Requires careful consideration to avoid memory leaks or unexpected behavior.
  • May not be suitable for all routes (e.g., routes with frequently changing data).
  • Increased development complexity.

Best Practices

  • Strive for a granular approach, caching only specific routes that benefit from reuse.
  • Be mindful of data persistence when reusing components that manage dynamic data.
  • Consider using techniques like lazy loading to further optimize component lifecycles.

--

--

Brajraj Agrawal
Brajraj Agrawal

Written by Brajraj Agrawal

Words that Build | Full-Stack Developer (MEAN Stack). Turning ideas into web apps for 8+ years.