The fundamentals of routing handle the basics of mapping URLs to components. Advanced routing techniques take that further: preloading strategies that download lazy-loaded chunks in the background after the initial page load (reducing the perceived load time for secondary pages), nested router outlets for complex layouts with persistent navigation sidebars, and route animations that add polished page transitions. Mastering these makes the difference between an Angular application that feels like a web app and one that feels like a native application.
Preloading Strategies
| Strategy | Behaviour | Use For |
|---|---|---|
NoPreloading (default) |
Load lazy modules only when navigated to | Low bandwidth environments, large apps |
PreloadAllModules |
Preload all lazy modules after initial load | Apps where all sections are likely visited |
Custom: QuicklinkStrategy |
Preload modules for links visible in viewport | Optimal — preloads what user will likely visit next |
Custom: data: { preload: true } |
Preload only routes marked with a flag | Manual control — preload critical paths only |
Custom: NetworkAwareStrategy |
Preload only on fast connections (4G/WiFi) | Mobile-first apps with data-conscious users |
Route Animation States
| State | When | Typical Use |
|---|---|---|
void |
Element does not exist in the DOM | Before entering or after leaving |
* (wildcard) |
Any state | Match any transition |
Custom name ('in') |
Named state defined by trigger | Specific visual states |
:enter |
Alias for void => * |
Element entering the DOM |
:leave |
Alias for * => void |
Element leaving the DOM |
PreloadAllModules is simple but preloads everything regardless of relevance. A custom strategy with data: { preload: true } on selected routes gives you fine-grained control — preload the dashboard and tasks pages (the most likely next destinations), but not the admin panel (rarely visited).@routeAnimation trigger on the <router-outlet>‘s container element. Read the animation state from the currently activated route’s data.animation property using outlet.activatedRouteData?.['animation']. Define different states for different pages (like 'tasks', 'detail') so you can have directional slide animations — sliding right when going from list to detail, left when going back.BrowserAnimationsModule (or provideAnimations() in standalone bootstrap). Without it, Angular silently ignores animation triggers and no errors appear — animations simply do not run. Also remember that animations that use position: absolute to overlap entering and leaving routes require a parent container with position: relative and defined dimensions — without this, the entering and leaving components overlap in unexpected ways during the transition.Complete Advanced Routing Examples
// ── Custom preloading strategy ────────────────────────────────────────────
// core/strategies/selective-preload.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of, timer } from 'rxjs';
import { switchMap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class SelectivePreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// Only preload routes marked with data: { preload: true }
if (route.data?.['preload'] !== true) return of(null);
// Delay preload by 2 seconds — don't compete with initial load
return timer(2000).pipe(switchMap(() => load()));
}
}
// Network-aware preloading strategy
@Injectable({ providedIn: 'root' })
export class NetworkAwarePreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data?.['preload'] !== true) return of(null);
// Check connection type via Navigator API
const connection = (navigator as any).connection;
const effectiveType = connection?.effectiveType ?? '4g';
// Only preload on fast connections
if (['slow-2g', '2g', '3g'].includes(effectiveType)) return of(null);
return load();
}
}
// app.config.ts — configure preloading
import { provideRouter, withPreloading } from '@angular/router';
export const appConfig = {
providers: [
provideRouter(
routes,
withPreloading(SelectivePreloadingStrategy),
),
],
};
// Routes — mark which should be preloaded
export const routes: Routes = [
{ path: 'tasks', data: { preload: true, animation: 'tasks' }, loadChildren: ... },
{ path: 'admin', data: { preload: false, animation: 'admin' }, loadChildren: ... },
{ path: 'help', data: { preload: true, animation: 'help' }, loadChildren: ... },
];
// ── Nested layouts with named router outlets ──────────────────────────────
// Persistent sidebar layout
// app.routes.ts
export const routes: Routes = [
{
path: 'tasks',
canActivate: [authGuard],
component: TasksLayoutComponent, // layout component with sidebar
children: [
{ path: '', component: TaskListComponent },
{ path: 'new', component: TaskFormComponent },
{ path: ':id', component: TaskDetailComponent },
],
},
];
// tasks-layout.component.ts — persistent sidebar + dynamic content area
@Component({
selector: 'app-tasks-layout',
standalone: true,
imports: [RouterOutlet, CommonModule, TaskSidebarComponent],
template: `
<div class="tasks-layout">
<aside class="tasks-layout__sidebar">
<app-task-sidebar [stats]="store.stats()"></app-task-sidebar>
</aside>
<main class="tasks-layout__content">
<router-outlet></router-outlet> <!-- child routes render here -->
</main>
</div>
`,
})
export class TasksLayoutComponent implements OnInit {
store = inject(TaskStore);
ngOnInit(): void { this.store.loadAll(); }
}
// ── Route animations ──────────────────────────────────────────────────────
// shared/animations/route.animations.ts
import {
trigger, transition, style, animate, query, group,
animateChild,
} from '@angular/animations';
export const routeAnimations = trigger('routeAnimations', [
// Fade animation for all transitions
transition('* <=> *', [
query(':enter, :leave', [
style({ position: 'absolute', width: '100%' }),
], { optional: true }),
group([
query(':leave', [
animate('200ms ease-out', style({ opacity: 0, transform: 'translateX(-20px)' })),
], { optional: true }),
query(':enter', [
style({ opacity: 0, transform: 'translateX(20px)' }),
animate('200ms 100ms ease-in', style({ opacity: 1, transform: 'translateX(0)' })),
], { optional: true }),
]),
]),
]);
// Slide animation — direction based on route order
export const slideAnimation = trigger('slideAnimation', [
transition('tasks => detail', [
style({ position: 'relative' }),
query(':enter, :leave', [style({ position: 'absolute', top: 0, left: 0, width: '100%' })]),
query(':enter', [style({ left: '100%' })]),
query(':leave', animateChild()),
group([
query(':leave', [animate('300ms ease-out', style({ left: '-100%' }))]),
query(':enter', [animate('300ms ease-out', style({ left: '0%' }))]),
]),
query(':enter', animateChild()),
]),
transition('detail => tasks', [
style({ position: 'relative' }),
query(':enter, :leave', [style({ position: 'absolute', top: 0, left: 0, width: '100%' })]),
query(':enter', [style({ left: '-100%' })]),
query(':leave', animateChild()),
group([
query(':leave', [animate('300ms ease-out', style({ left: '100%' }))]),
query(':enter', [animate('300ms ease-out', style({ left: '0%' }))]),
]),
query(':enter', animateChild()),
]),
]);
// app.component.ts — bind animation trigger
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
animations: [routeAnimations],
template: `
<div [@routeAnimations]="getRouteAnimationData(outlet)"
style="position: relative; overflow: hidden;">
<router-outlet #outlet="outlet"></router-outlet>
</div>
`,
})
export class AppComponent {
getRouteAnimationData(outlet: RouterOutlet): string | undefined {
return outlet?.activatedRouteData?.['animation'];
}
}
// app.config.ts — enable animations
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig = {
providers: [
provideAnimations(), // required for Angular animations to work
provideRouter(routes, withPreloading(SelectivePreloadingStrategy)),
],
};
How It Works
Step 1 — Preloading Strategies Hook Into the Router’s Idle Time
After the initial navigation completes and the application is idle, the Router calls preload(route, loadFn) for every lazy-loaded route in the configuration. Your strategy function decides whether to call loadFn() (which triggers the download and module registration) or return of(null) (skip). The downloaded module is cached — when the user actually navigates to that route, Angular uses the cached version and activates instantly.
Step 2 — Nested Layouts Use Child Router Outlets
A layout component that contains a <router-outlet> acts as the host for its child routes. The layout’s own DOM (sidebar, header, breadcrumbs) persists across child route changes — only the content inside the outlet changes. This is the correct architecture for pages with persistent navigation panels: the layout component is activated once by the parent route and stays alive while any child route is active. Child components never re-render the layout.
Step 3 — Route Animation Data Drives Transitions
Adding data: { animation: 'tasks' } to a route lets the root component read the current route’s animation state with outlet.activatedRouteData?.['animation']. Binding this to [@routeAnimations]="state" on the outlet’s container element triggers the animation whenever the state changes (navigation). Different state values can trigger different animation styles — slide right going into detail, slide left going back to list.
Step 4 — group() and query() Coordinate Multi-Element Animations
When navigating, both the leaving component and entering component exist simultaneously during the transition. query(':enter') selects the entering component’s host element; query(':leave') selects the leaving one. group([]) runs multiple animations in parallel — slide the leaving element out while simultaneously sliding the entering element in. Without group(), the leave animation would complete before the enter animation starts.
Step 5 — withInMemoryScrolling Restores Scroll Position
By default, Angular does not manage scroll position on navigation — browsers maintain their own position. withInMemoryScrolling({ scrollPositionRestoration: 'top' }) scrolls to the top of the page on every navigation. 'enabled' restores the previous scroll position when navigating back. anchorScrolling: 'enabled' supports navigating to named anchors (/tasks#overdue). These options provide the scroll behaviour users expect from traditional multi-page applications.
Common Mistakes
Mistake 1 — Animations not working — missing provideAnimations()
❌ Wrong — animations silently fail without the provider:
// app.config.ts
export const appConfig = {
providers: [
provideRouter(routes), // no provideAnimations()!
// Animation triggers in templates do nothing — no error shown
],
};
✅ Correct — always include provideAnimations():
import { provideAnimations } from '@angular/platform-browser/animations';
providers: [ provideAnimations(), provideRouter(routes) ]
Mistake 2 — Preloading all modules regardless of relevance
❌ Wrong — PreloadAllModules downloads admin, reports, and settings on every page load:
provideRouter(routes, withPreloading(PreloadAllModules))
// Downloads admin panel code for all users, even those who will never see it
✅ Correct — selective preloading for likely-visited routes only:
provideRouter(routes, withPreloading(SelectivePreloadingStrategy))
// Only preloads routes with data: { preload: true }
Mistake 3 — Overlapping route animation elements without position: absolute
❌ Wrong — entering and leaving elements stack vertically during transition:
transition('* <=> *', [
query(':enter, :leave', style({ width: '100%' })), // no position: absolute!
// Elements stack — layout jumps during animation
])
✅ Correct — use absolute positioning for overlap:
transition('* <=> *', [
query(':enter, :leave', style({ position: 'absolute', width: '100%' })),
// Elements overlap cleanly during animation
])
Quick Reference
| Task | Code |
|---|---|
| Preload all | provideRouter(routes, withPreloading(PreloadAllModules)) |
| Selective preload | Custom strategy + data: { preload: true } on routes |
| Scroll to top | withInMemoryScrolling({ scrollPositionRestoration: 'top' }) |
| Enable animations | provideAnimations() in app.config.ts |
| Route animation data | data: { animation: 'stateName' } on route |
| Read animation state | outlet.activatedRouteData?.['animation'] |
| Animation trigger | [@myTrigger]="animationState" on outlet container |
| Nested layout | Parent route component with <router-outlet> + children array |
| Parallel animations | group([query(':enter', [...]), query(':leave', [...])]) |