Information about my app (Angular 12):
So I wanted to implement Angular´s RouteReuseStrategy for the following behaviour:
Whenever a user navigates from list -> detail page and uses back button, the list component should be reused (detect back button trigger)
Whenever a user navigates to the list component from a different module / area the list component should not be reused
Whenever a user navigates from detail to another detail page, the detail component should be reused (default behaviour?)
Whenever a user leaves a module, by navigating to another or logging out, the stored components should be cleared / destroyed
Current Situation:
I implemented a custom RouteReuseStrategy, which works and the list component is reused ✓
I wanted to check the areatag inside the route, but the ActivatedRouteSnapshots are empty ✕
Detecting the backbutton press, but events fire to often and it breaks if I implement a basic back flag ✕
What is missing?
Detect backbutton navigation and modify the reuse of the component
Detect which module the route is part of to modify reuse or clean up the stored components
Code:
Example route in Module A
{
path: 'lista',
component: ListAComponent,
data: {
title: 'List overview',
areaCategory: AreaCategory.A,
reuseRoute: true,
},
},
{
path: 'lista/:id',
component: DetailAComponent,
data: {
title: 'Detail',
areaCategory: AreaCategory.A,
reuseRoute: false,
},
},
Example route Module B
{
path: 'listb',
component: ListBComponent,
data: {
title: 'List overview',
areaCategory: AreaCategory.B,
reuseRoute: true,
},
},
{
path: 'listb/:id',
component: DetailBComponent,
data: {
title: 'Detail',
areaCategory: AreaCategory.B,
reuseRoute: false,
},
},
app.module.ts
providers: [
{
provide: RouteReuseStrategy,
useClass: CustomReuseRouteStrategy,
}
],
Should be fine here globally or do I need to move it to each of the 3 modules?
ReuseRouteStrategy
@Injectable()
export class CustomReuseRouteStrategy implements RouteReuseStrategy {
private handlers: { [key: string]: DetachedRouteHandle } = {};
// Detect Backbutton-navigation
back = false;
constructor(location: LocationStrategy) {
location.onPopState(() => {
this.back = true;
});
}
shouldDetach(route: ActivatedRouteSnapshot): boolean {
if (!route.routeConfig || route.routeConfig.loadChildren) {
return false;
}
// Check route.data.reuse whether this route should be re used or not
let shouldReuse = false;
if (
route.routeConfig.data &&
route.routeConfig.data.reuseRoute &&
typeof route.routeConfig.data.reuseRoute === 'boolean'
) {
shouldReuse = route.routeConfig.data.reuseRoute;
}
return shouldReuse;
}
store(route: ActivatedRouteSnapshot, handler: DetachedRouteHandle): void {
if (handler) {
this.handlers[this.getUrl(route)] = handler;
}
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
if (!this.back) {
return false;
}
return !!this.handlers[this.getUrl(route)];
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
if (!this.back || !route.routeConfig || route.routeConfig.loadChildren) {
return null;
}
//this.back = false; -> does not work fires to often
return this.handlers[this.getUrl(route)];
}
shouldReuseRoute(future: ActivatedRouteSnapshot, current: ActivatedRouteSnapshot): boolean {
/** We only want to reuse the route if the data of the route config contains a reuse true boolean */
let reUseUrl = false;
if (future.routeConfig && future.routeConfig.data && typeof future.routeConfig.data.reuseRoute === 'boolean') {
reUseUrl = future.routeConfig.data.reuseRoute;
}
//const defaultReuse = future.routeConfig === current.routeConfig; -> used for navigating to same component but routeConfigs are empty therefore always match?
return reUseUrl;
}
private getUrl(route: ActivatedRouteSnapshot): string {
if (route.routeConfig) {
const url = route.routeConfig.path;
return url;
}
}
clearHandles() {
for (const key in this.handlers) {
if (this.handlers[key]) {
this.destroyHandle(this.handlers[key]);
}
}
this.handlers = {};
}
private destroyHandle(handle: DetachedRouteHandle): void {
const componentRef: ComponentRef<any> = handle['componentRef'];
if (componentRef) {
componentRef.destroy();
}
}
}
I noticed _routerState inside the ActivatedRouteSnapshot holds the url, which could be used to differentiate the modules, but I´d rather check the areaCategory from the route data but weirdly, the future & current ActivatedRouteSnapshots in the shouldReuseRoute method are mostly empty
Also I am not sure about using internal values like _routerState, since I heard these are not fixed an can change at any time
Log of empty future & current snapshot (only url is useful)
Why is it AppComponent instead of Detail or ListAComponent? Maybe thats why the data is empty?
I need to get the correct route / component to access the areaCategory to implement the desired behavior.
As requested here is a simple stackblitz with my setup
If I missed something, please let me know, would really appreciate the help
This respects your conditions
list -> detail -> list
navigationsareaCategory
tagI'm adding you some extra comments and a diagram to understand RouteReuseStrategy
calling which will make more clear how to use it.
Follows the diagram and the pseudocode that shows how the strategy is used by Angular core (it is based on my observations, I have found no official documentation):
transition(current, future) {
if (shouldReuseRoute(future, current)) {
// No navigation is performed, same route is recycled
return current;
} else {
if (shouldDetach(current)) {
// If not reused and shouldDetach() == true then store
store(current, getRouteHandler(current));
}
if (shouldAttach(future)) {
// If not reused and shouldAttach() == true then retrieve
return createRouteFromHandler(retrieve(future));
} else {
// If shouldAttach() == false do not recycle
return future;
}
}
}
Of course, this is an example. getRouteHandler
and createRouteFromHandler
are introduced just as an example and no distinction is made between route component, route instance, and route snapshot.
@Injectable()
export class CustomReuseRouteStrategy implements RouteReuseStrategy {
private handlers: { [key: string]: DetachedRouteHandle } = {};
clearHandlers() {
Object.keys(this.handlers).forEach(h => {
// https://github.com/angular/angular/issues/15873
(h as any).componentRef.destroy();
})
this.handlers = {};
}
areSameArea(future: ActivatedRouteSnapshot, current: ActivatedRouteSnapshot): boolean {
return future.routeConfig.data && current.routeConfig.data
&& future.routeConfig.data.areaCategory === current.routeConfig.data.areaCategory;
}
/**
* This function decides weather the current route should be kept.
* If this function returns `true` nor attach or detach procedures are called
* (hence none of shouldDetach, shouldAttach, store, retrieve). If this function
* returns `false` an attach/detach procedure is initiated.
*/
shouldReuseRoute(future: ActivatedRouteSnapshot, current: ActivatedRouteSnapshot): boolean {
console.log('shouldReuseRoute', future, current);
if (!this.areSameArea(future, current)) {
// Changed area, clear the cache
this.clearHandlers();
}
return this.getUrl(future) === this.getUrl(current);
}
/**
* DETACH PROCEDURE: if performing a detach, this function is called, if returns
* `true` then store is called, to store the current context.
*/
shouldDetach(route: ActivatedRouteSnapshot): boolean {
console.log('shouldDetach', route);
// We always detach them (you never mentioned pages that are not recycled
// by default)
return true;
}
store(route: ActivatedRouteSnapshot, handler: DetachedRouteHandle): void {
console.log('store', route, this.getUrl(route));
if (!handler && this.getUrl(route)) return;
this.handlers[this.getUrl(route)] = handler;
}
/**
* ATTACH PROCEDURE: if performing an attach, this function is called, if returns
* `true` then retrieve is called, to store the current context.
*/
shouldAttach(route: ActivatedRouteSnapshot): boolean {
console.log('shouldAttach', route);
return !!this.handlers[this.getUrl(route)];
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
console.log('retrieve', route, this.getUrl(route));
return this.getUrl(route) && this.handlers[this.getUrl(route)];
}
private getUrl(route: ActivatedRouteSnapshot): string {
// todo : not sure this behaves properly in case of parametric routes
return route.routeConfig && route.routeConfig.path;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With