Maîtrisez RouteReuseStrategy Angular : 5 méthodes, cache de composants, stratégie personnalisée et zéro fuite mémoire.
L'interface RouteReuseStrategy : les 5 méthodes
L'interface RouteReuseStrategy est définie dans @angular/router. Elle expose 5 méthodes abstraites que vous devez implémenter. Angular les appelle dans un ordre précis à chaque navigation.
// Définition de l'interface (source Angular)
export abstract class RouteReuseStrategy {
// 1. Décide si la route doit être mise en cache avant navigation
abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
// 2. Stocke le handle de la route détachée
abstract store(
route: ActivatedRouteSnapshot,
handle: DetachedRouteHandle | null
): void;
// 3. Décide si un handle en cache doit être réutilisé à l'arrivée
abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
// 4. Retourne le handle mis en cache (ou null)
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null;
// 5. Décide si la route ACTUELLE peut être réutilisée (navigation vers la même route)
abstract shouldReuseRoute(
future: ActivatedRouteSnapshot,
curr: ActivatedRouteSnapshot
): boolean;
}
Le flux d'exécution lors d'une navigation de /orders vers /clients est le suivant :
| # | Méthode appelée | Route concernée | Question posée |
|---|---|---|---|
| 1 | shouldReuseRoute |
orders → clients | Même configuration de route ? (non → continue) |
| 2 | shouldDetach |
/orders (route quittée) |
Faut-il mettre orders en cache ? |
| 3 | store |
/orders |
Si oui → stocker le handle |
| 4 | shouldAttach |
/clients (route cible) |
Un handle en cache existe-t-il pour clients ? |
| 5 | retrieve |
/clients |
Si oui → retourner le handle stocké |
Ordre d'appel en résumé :
- shouldReuseRoute → appelé en premier, sur TOUTE navigation
- shouldDetach → store → appelés sur la route quittée (si shouldReuseRoute retourne false)
- shouldAttach → retrieve → appelés sur la route cible (si shouldReuseRoute retourne false)
shouldReuseRoute : le comportement par défaut
C'est la méthode la plus simple mais la plus fréquemment appelée. Angular l'invoque à chaque navigation, même mineure. L'implémentation par défaut d'Angular (DefaultUrlSerializer) compare simplement les configurations de route :
// Implémentation par défaut Angular (BaseRouteReuseStrategy)
shouldReuseRoute(
future: ActivatedRouteSnapshot,
curr: ActivatedRouteSnapshot
): boolean {
// Retourne true si c'est exactement la même définition de route
// Cela signifie : même path pattern, même composant associé
return future.routeConfig === curr.routeConfig;
}
Concrètement, ce comportement par défaut signifie :
| Navigation | shouldReuseRoute retourne | Effet |
|---|---|---|
/orders/1 → /orders/2 |
true |
Même route (orders/:id), composant CONSERVÉ, ngOnInit non rappelé |
/orders → /clients |
false |
Routes différentes → shouldDetach/shouldAttach déclenchés |
/orders → /orders |
true |
Même URL, composant CONSERVÉ (pas de re-render) |
Le cas /orders/1 → /orders/2 est le plus délicat. Angular ne rappelle pas ngOnInit car il considère que c'est la même route. Pour détecter le changement de paramètre, vous devez observer ActivatedRoute.params ou ActivatedRoute.paramMap :
// Écouter les changements de paramètre dans la même route
@Component({ selector: 'app-order-detail', template: '...' })
export class OrderDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private ordersService = inject(OrdersService);
order = signal<Order | null>(null);
ngOnInit(): void {
// paramMap émet à chaque changement d'ID même si shouldReuseRoute = true
this.route.paramMap.pipe(
map(params => params.get('id') ?? ''),
switchMap(id => this.ordersService.getOrder(id))
).subscribe(order => this.order.set(order));
}
}
route.snapshot.params['id'] dans ngOnInit au lieu d'observer route.paramMap, vous ne verrez jamais les changements de paramètre quand shouldReuseRoute retourne true.
Forcer le re-rendering sur la même route
Si vous voulez qu'Angular détruise et recrée le composant même sur la même route (comportement "toujours neuf"), surchargez shouldReuseRoute pour retourner toujours false. À utiliser avec précaution :
// Stratégie "toujours recréer" — utile pour certains flux d'onboarding
@Injectable()
export class NeverReuseStrategy extends BaseRouteReuseStrategy {
override shouldReuseRoute(
future: ActivatedRouteSnapshot,
curr: ActivatedRouteSnapshot
): boolean {
// Vérifier un flag optionnel dans la config de route
if (future.data['alwaysRefresh'] === true) {
return false; // Forcer la recréation pour cette route
}
// Comportement par défaut pour les autres routes
return future.routeConfig === curr.routeConfig;
}
}
// Route configurée pour toujours se rafraîchir
export const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
data: { alwaysRefresh: true }, // Toujours recréer ce composant
},
{
path: 'orders/:id',
component: OrderDetailComponent,
// Pas de alwaysRefresh → comportement par défaut
},
];
Créer une stratégie personnalisée complète
Voici l'implémentation complète d'une stratégie de mise en cache sélective. Elle met en cache uniquement les routes marquées avec data: { reuse: true }, ce qui vous donne un contrôle granulaire route par route.
// src/app/core/strategies/custom-route-reuse.strategy.ts
import { Injectable } from '@angular/core';
import {
RouteReuseStrategy,
ActivatedRouteSnapshot,
DetachedRouteHandle,
Route,
} from '@angular/router';
@Injectable()
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
// Map pour stocker les handles — clé = configuration de route, valeur = handle
private readonly cache = new Map<Route, DetachedRouteHandle>();
// 1. Faut-il détacher (mettre en cache) la route quittée ?
shouldDetach(route: ActivatedRouteSnapshot): boolean {
// Seules les routes marquées reuse:true seront mises en cache
return route.data['reuse'] === true;
}
// 2. Stocker le handle du composant détaché
store(
route: ActivatedRouteSnapshot,
handle: DetachedRouteHandle | null
): void {
if (handle === null) {
// Angular nous demande de supprimer l'entrée (navigation de nettoyage)
this.cache.delete(route.routeConfig!);
return;
}
if (route.data['reuse'] === true && route.routeConfig) {
this.cache.set(route.routeConfig, handle);
}
}
// 3. La route cible doit-elle réutiliser un handle en cache ?
shouldAttach(route: ActivatedRouteSnapshot): boolean {
// Deux conditions : route marquée ET handle disponible en cache
return (
route.data['reuse'] === true &&
route.routeConfig !== null &&
this.cache.has(route.routeConfig)
);
}
// 4. Retourner le handle mis en cache pour la route cible
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
if (!route.routeConfig || !this.cache.has(route.routeConfig)) {
return null;
}
return this.cache.get(route.routeConfig) ?? null;
}
// 5. Réutiliser le composant actuel si même routeConfig (comportement standard)
shouldReuseRoute(
future: ActivatedRouteSnapshot,
curr: ActivatedRouteSnapshot
): boolean {
return future.routeConfig === curr.routeConfig;
}
}
Configurer les routes avec le flag reuse
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { OrdersComponent } from './orders/orders.component';
import { ClientsComponent } from './clients/clients.component';
import { StatsComponent } from './stats/stats.component';
import { OrderDetailComponent } from './order-detail/order-detail.component';
export const routes: Routes = [
{
path: 'orders',
component: OrdersComponent,
data: { reuse: true }, // Mis en cache — liste volumineuse
},
{
path: 'clients',
component: ClientsComponent,
data: { reuse: true }, // Mis en cache — grille filtrable
},
{
path: 'stats',
component: StatsComponent,
// Pas de reuse → toujours recréé (données temps réel)
},
{
path: 'orders/:id',
component: OrderDetailComponent,
// Pas de reuse → formulaire toujours propre à l'ouverture
},
];
Enregistrer la stratégie dans l'application
L'enregistrement se fait via le provider RouteReuseStrategy. Deux approches selon votre architecture :
// Approche standalone (Angular 14+) — src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { RouteReuseStrategy } from '@angular/router';
import { routes } from './app.routes';
import { CustomRouteReuseStrategy } from './core/strategies/custom-route-reuse.strategy';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
// Remplace l'implémentation par défaut d'Angular
{
provide: RouteReuseStrategy,
useClass: CustomRouteReuseStrategy,
},
],
};
// Approche NgModule (Angular classic) — src/app/app.module.ts
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { RouteReuseStrategy } from '@angular/router';
import { CustomRouteReuseStrategy } from './core/strategies/custom-route-reuse.strategy';
import { routes } from './app.routes';
@NgModule({
imports: [RouterModule.forRoot(routes)],
providers: [
{
provide: RouteReuseStrategy,
useClass: CustomRouteReuseStrategy,
},
],
})
export class AppModule {}
src/app/core/strategies/. Créez un barrel index.ts dans ce dossier pour des imports propres. La stratégie est un service singleton — Angular l'injecte là où RouteReuseStrategy est requis (uniquement par le Router en interne).
shouldDetach, store, shouldAttach, retrieve en détail
Ces quatre méthodes forment le cœur du mécanisme de cache. Comprendre leur interaction précise est essentiel pour éviter les bugs.
shouldDetach : choisir ce qu'on met en cache
Cette méthode est appelée quand l'utilisateur quitte une route. Elle doit retourner true si le composant doit être préservé. Le paramètre route est un snapshot de la route que l'on quitte.
// Stratégie fine : reuse conditionnel selon l'état du composant
shouldDetach(route: ActivatedRouteSnapshot): boolean {
// Option 1 : flag statique dans la config de route
if (route.data['reuse'] === true) return true;
// Option 2 : flag dynamique défini par le composant lui-même
// Le composant peut définir ce flag selon son état interne
if (route.data['reuseWhenDirty'] === true) {
// Récupérer un service pour vérifier l'état du formulaire
// (pattern avancé — voir section cas d'usage)
return true;
}
return false; // Par défaut : ne pas mettre en cache
}
store : persister le handle
Appelée juste après que shouldDetach retourne true. Angular vous passe le DetachedRouteHandle — l'objet encapsulant le composant. Vous pouvez également recevoir null (Angular nettoie le cache).
// store avec logging pour le debug
store(
route: ActivatedRouteSnapshot,
handle: DetachedRouteHandle | null
): void {
const key = route.routeConfig;
if (handle === null) {
// Nettoyage demandé par Angular (ex: navigation forcée ou guard de déactivation)
if (key) this.cache.delete(key);
return;
}
if (key) {
this.cache.set(key, handle);
// En dev : tracer quelles routes sont en cache
if (isDevMode()) {
console.log('[ReuseStrategy] Mis en cache :', route.routeConfig?.path);
console.log('[ReuseStrategy] Taille du cache :', this.cache.size);
}
}
}
shouldAttach : décider le réattachement
Appelée quand l'utilisateur arrive sur une route. Retourner true signifie "utilise le composant en cache au lieu d'en créer un nouveau". Si vous retournez true mais que retrieve retourne null, Angular lancera une erreur.
// shouldAttach robuste avec vérification complète
shouldAttach(route: ActivatedRouteSnapshot): boolean {
// Vérification 1 : la route est-elle marquée pour réutilisation ?
if (route.data['reuse'] !== true) return false;
// Vérification 2 : routeConfig valide ?
if (!route.routeConfig) return false;
// Vérification 3 : handle réellement présent en cache ?
const hasHandle = this.cache.has(route.routeConfig);
// IMPORTANT : ne retourner true que si retrieve() peut retourner un handle valide
// Sinon Angular lève une erreur runtime
return hasHandle;
}
retrieve : restituer le handle
Appelée immédiatement après shouldAttach si elle retourne true. Doit retourner le handle stocké dans store.
// retrieve avec protection contre null
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
// Double vérification — toujours defensive
if (!route.routeConfig) return null;
if (!this.cache.has(route.routeConfig)) return null;
const handle = this.cache.get(route.routeConfig);
// Retourner undefined comme null pour Angular
return handle ?? null;
}
shouldAttach retourne true, retrieve DOIT retourner un handle non-null. Ces deux méthodes doivent être logiquement cohérentes. Une façon simple de garantir cette cohérence : partagez la même condition de vérification du cache.
Clés de cache intelligentes : path, params, query
L'implémentation basique utilise route.routeConfig comme clé — un objet JavaScript. Cette approche a une limite critique : si vous naviguez de /orders/1 vers /orders/2 et que ces deux routes ont le même routeConfig (car c'est la même définition orders/:id), le cache ne distingue pas les deux.
Pour un cache basé sur l'URL complète (incluant les paramètres), construisez une clé de type string :
// Stratégie avec clés URL-based — gère les paramètres de route
@Injectable()
export class SmartRouteReuseStrategy implements RouteReuseStrategy {
private readonly cache = new Map<string, DetachedRouteHandle>();
// Construire une clé unique à partir du path + params
private buildKey(route: ActivatedRouteSnapshot): string {
// Construire le path en remplaçant les segments de paramètres
// Ex: "orders/:id" avec params {id: '42'} → "orders/42"
const path = route.pathFromRoot
.filter(r => r.routeConfig?.path)
.map(r => {
let segment = r.routeConfig!.path!;
// Remplacer chaque paramètre :param par sa valeur réelle
Object.entries(r.params).forEach(([key, value]) => {
segment = segment.replace(`:${key}`, String(value));
});
return segment;
})
.join('/');
// Optionnel : inclure les query params pour différencier ?page=1 de ?page=2
const queryString = Object.entries(route.queryParams)
.sort(([a], [b]) => a.localeCompare(b)) // Tri pour stabilité
.map(([k, v]) => `${k}=${v}`)
.join('&');
return queryString ? `${path}?${queryString}` : path;
}
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return route.data['reuse'] === true;
}
store(
route: ActivatedRouteSnapshot,
handle: DetachedRouteHandle | null
): void {
const key = this.buildKey(route);
if (handle === null) {
this.cache.delete(key);
} else if (route.data['reuse'] === true) {
this.cache.set(key, handle);
}
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return route.data['reuse'] === true && this.cache.has(this.buildKey(route));
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return this.cache.get(this.buildKey(route)) ?? null;
}
shouldReuseRoute(
future: ActivatedRouteSnapshot,
curr: ActivatedRouteSnapshot
): boolean {
return future.routeConfig === curr.routeConfig;
}
}
Avec cette approche, /orders/1 et /orders/2 ont des clés distinctes ("orders/1" et "orders/2"). Chaque combinaison path/params a son propre slot dans le cache. C'est le comportement attendu pour une app de type "onglets de détail".
| URL naviguée | Clé générée (buildKey) | Comportement |
|---|---|---|
/orders |
"orders" |
Une seule entrée pour la liste |
/orders/1 |
"orders/1" |
Cache distinct par ID |
/orders/2 |
"orders/2" |
Cache distinct par ID |
/orders?page=2 |
"orders?page=2" |
Cache distinct par page |
buildKey.
Gestion mémoire : TTL, limites et destroyHandle
Un cache sans gestion devient une fuite mémoire. Chaque DetachedRouteHandle conserve en mémoire le composant, son arbre DOM virtuel, ses subscriptions et ses données. Multipliez ça par 20 routes cachées, et votre application devient un gouffre de RAM.
Limiter la taille du cache (LRU simple)
// Stratégie avec limite LRU (Least Recently Used)
@Injectable()
export class BoundedCacheReuseStrategy implements RouteReuseStrategy {
private readonly MAX_CACHE_SIZE = 5; // Au maximum 5 composants en cache
// Map ordonnée : les entrées les plus récentes sont en fin de map
private readonly cache = new Map<string, {
handle: DetachedRouteHandle;
timestamp: number;
}>();
store(
route: ActivatedRouteSnapshot,
handle: DetachedRouteHandle | null
): void {
const key = route.routeConfig?.path ?? '';
if (!key) return;
if (handle === null) {
this.cache.delete(key);
return;
}
// Si cache plein, supprimer l'entrée la plus ancienne
if (this.cache.size >= this.MAX_CACHE_SIZE) {
const oldestKey = this.cache.keys().next().value;
if (oldestKey) {
this.destroyHandle(this.cache.get(oldestKey)!.handle);
this.cache.delete(oldestKey);
}
}
this.cache.set(key, {
handle,
timestamp: Date.now(),
});
}
// Libérer proprement le composant (Angular 17+)
private destroyHandle(entry: { handle: DetachedRouteHandle }): void {
// destroyDetachedRouteHandle est disponible depuis Angular 17
// pour libérer proprement la vue et déclencher ngOnDestroy
const componentRef = (entry.handle as any)?.componentRef;
if (componentRef?.destroy) {
componentRef.destroy(); // Déclenche ngOnDestroy + libère la ViewRef
}
}
// ... autres méthodes (shouldDetach, shouldAttach, retrieve, shouldReuseRoute)
}
TTL : expirer les composants après un délai
// Ajout d'un TTL de 5 minutes par entrée de cache
store(
route: ActivatedRouteSnapshot,
handle: DetachedRouteHandle | null
): void {
const key = this.buildKey(route);
if (handle === null) {
this.cache.delete(key);
return;
}
// Expirer les entrées périmées avant d'en ajouter une nouvelle
const TTL_MS = 5 * 60 * 1000; // 5 minutes
const now = Date.now();
for (const [cacheKey, entry] of this.cache.entries()) {
if (now - entry.timestamp > TTL_MS) {
this.cache.delete(cacheKey); // Supprimer les entrées expirées
}
}
if (route.data['reuse'] === true) {
this.cache.set(key, { handle, timestamp: now });
}
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
if (route.data['reuse'] !== true) return false;
const key = this.buildKey(route);
const entry = this.cache.get(key);
if (!entry) return false;
const TTL_MS = 5 * 60 * 1000;
// Ne réutiliser que si le cache n'a pas expiré
if (Date.now() - entry.timestamp > TTL_MS) {
this.cache.delete(key); // Nettoyer l'entrée périmée
return false;
}
return true;
}
ngOnDestroy et les composants en cache
Un composant mis en cache par RouteReuseStrategy n'a pas son ngOnDestroy appelé lors de la mise en cache. Il sera appelé uniquement quand le handle est détruit (fin de session, LRU eviction, ou destruction manuelle). Cela a des implications importantes :
- RxJS subscriptions : utilisez
takeUntilDestroyed()ouDestroyRefpour vous désabonner proprement - Intervals / setTimeout : stockez la référence et nettoyez dans
ngOnDestroy - Event listeners : toujours les retirer dans
ngOnDestroy - Données temps réel : implémenter
ngOnInit+ActivatedRoutepour détecter le retour sur la route - Web Sockets : gérer la reconnexion via un service partagé, pas dans le composant
// Pattern correct pour un composant qui peut être mis en cache
@Component({ selector: 'app-orders', template: '...' })
export class OrdersComponent implements OnInit, OnDestroy {
private destroyRef = inject(DestroyRef);
private route = inject(ActivatedRoute);
orders = signal<Order[]>([]);
ngOnInit(): void {
// takeUntilDestroyed se désabonne automatiquement quand DestroyRef émet
// Fonctionne correctement même avec RouteReuseStrategy
this.ordersService.getOrders()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => this.orders.set(data));
}
ngOnDestroy(): void {
// Appelé quand le handle est vraiment détruit (pas lors du détachement)
console.log('OrdersComponent définitivement détruit');
}
}
Tests unitaires et checklist de production
Tester une RouteReuseStrategy est straightforward : c'est une classe pure avec des méthodes qui reçoivent des snapshots et retournent des booleans ou des handles. Pas besoin de TestBed pour les tests unitaires des méthodes de décision.
// custom-route-reuse.strategy.spec.ts
import { CustomRouteReuseStrategy } from './custom-route-reuse.strategy';
import { ActivatedRouteSnapshot, Route } from '@angular/router';
describe('CustomRouteReuseStrategy', () => {
let strategy: CustomRouteReuseStrategy;
// Fabrique un ActivatedRouteSnapshot minimal pour les tests
function makeRoute(config: Partial<Route> = {}, data: Record<string, unknown> = {}): ActivatedRouteSnapshot {
const snapshot = new ActivatedRouteSnapshot();
(snapshot as any).routeConfig = config;
(snapshot as any).data = data;
(snapshot as any).pathFromRoot = [snapshot];
(snapshot as any).params = {};
(snapshot as any).queryParams = {};
return snapshot;
}
beforeEach(() => {
strategy = new CustomRouteReuseStrategy();
});
describe('shouldDetach', () => {
it('retourne true pour une route avec reuse:true', () => {
const route = makeRoute({ path: 'orders' }, { reuse: true });
expect(strategy.shouldDetach(route)).toBe(true);
});
it('retourne false sans flag reuse', () => {
const route = makeRoute({ path: 'orders' });
expect(strategy.shouldDetach(route)).toBe(false);
});
});
describe('store et shouldAttach', () => {
it('shouldAttach retourne false pour une route non mise en cache', () => {
const route = makeRoute({ path: 'orders' }, { reuse: true });
expect(strategy.shouldAttach(route)).toBe(false); // Cache vide
});
it('shouldAttach retourne true après store', () => {
const routeConfig: Route = { path: 'orders' };
const route = makeRoute(routeConfig, { reuse: true });
const fakeHandle = {} as any; // Handle fictif
// Simuler le cycle store → shouldAttach
strategy.store(route, fakeHandle);
expect(strategy.shouldAttach(route)).toBe(true);
});
it('retrieve retourne le handle stocké', () => {
const routeConfig: Route = { path: 'orders' };
const route = makeRoute(routeConfig, { reuse: true });
const fakeHandle = { key: 'test' } as any;
strategy.store(route, fakeHandle);
expect(strategy.retrieve(route)).toBe(fakeHandle);
});
it('store avec null supprime l\'entrée du cache', () => {
const routeConfig: Route = { path: 'orders' };
const route = makeRoute(routeConfig, { reuse: true });
const fakeHandle = {} as any;
strategy.store(route, fakeHandle);
strategy.store(route, null); // Nettoyage
expect(strategy.shouldAttach(route)).toBe(false);
});
});
describe('shouldReuseRoute', () => {
it('retourne true pour la même routeConfig', () => {
const config: Route = { path: 'orders/:id' };
const future = makeRoute(config);
const curr = makeRoute(config);
expect(strategy.shouldReuseRoute(future, curr)).toBe(true);
});
it('retourne false pour des configs différentes', () => {
const future = makeRoute({ path: 'orders' });
const curr = makeRoute({ path: 'clients' });
expect(strategy.shouldReuseRoute(future, curr)).toBe(false);
});
});
});
Test d'intégration avec le Router
// Test d'intégration — vérifie que le composant n'est pas recréé
import { TestBed } from '@angular/core/testing';
import { provideRouter, Router, RouteReuseStrategy } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
describe('RouteReuseStrategy — intégration', () => {
it('ne recrée pas le composant après navigation retour', async () => {
await TestBed.configureTestingModule({
providers: [
provideRouter(routes),
{ provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy },
],
}).compileComponents();
const harness = await RouterTestingHarness.create('/orders');
const ordersComp = harness.routeNativeElement;
const spy = jest.spyOn(console, 'log');
// Naviguer ailleurs
await harness.navigateByUrl('/clients');
// Revenir sur orders
await harness.navigateByUrl('/orders');
// ngOnInit ne doit pas être rappelé (composant réattaché depuis le cache)
expect(spy).not.toHaveBeenCalledWith(expect.stringContaining('ngOnInit'));
});
});
Checklist avant mise en production
- Stratégie enregistrée dans
appConfigouAppModuleproviders - Routes à mettre en cache marquées avec
data: { reuse: true } - Cohérence entre
shouldAttachetretrieve: même condition de vérification - Taille du cache limitée (LRU ou TTL) pour éviter les fuites mémoire
-
ngOnDestroypropre dans tous les composants cachés (takeUntilDestroyedpour RxJS) - Données temps réel gérées via
ActivatedRoute.params(pas uniquement dansngOnInit) - Tests unitaires des 5 méthodes avec cas positifs et négatifs
- Test d'intégration vérifiant que
ngOnInitn'est pas rappelé à la réattachement - Profil mémoire testé dans Chrome DevTools (heap snapshot avant/après navigation)
- Comportement validé en mode production (
ng build --configuration production)
reuse: true + routeConfig comme clé). Ajoutez la complexité — clés URL-based, TTL, LRU — uniquement si les cas d'usage l'exigent. Une stratégie over-engineered est plus difficile à déboguer qu'un re-render de 200ms.