Advanced Angular Routing: Resolvers, Child Routes, Preloading and Transitions

Angular Tutorial: Beginner to Advanced · 17. Advanced routing, rendering and SEO · 5 min read · Updated

You already know routes, parameters, guards and lazy loading. This lesson adds the tools used in larger apps: layouts with child routes, loading data before a page opens, smarter preloading and smooth page transitions.

What you will learn

  • Layouts with child routes
  • Resolvers and route data
  • canMatch, preloading and view transitions

Child routes and layouts

A parent route can render a layout, and its children appear inside the layout's own <router-outlet>.

// src/app/routing/account-layout.ts
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-account-layout',
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <h2>My account</h2>
    <nav aria-label="Account">
      <a routerLink="profile" routerLinkActive="active">Profile</a>
      <a routerLink="orders" routerLinkActive="active">Orders</a>
    </nav>
    <router-outlet />
  `,
  styles: `nav { display: flex; gap: 1rem; } .active { font-weight: bold; }`,
})
export class AccountLayout {}
// src/app/routing/account-pages.ts
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-account-profile',
  template: `<h3>Profile</h3><p>Section: {{ section() }}</p>`,
})
export class AccountProfile {
  // Filled from route data (see below).
  readonly section = input('');
}

export interface Order {
  id: number;
  total: number;
}

@Component({
  selector: 'app-account-orders',
  template: `
    <h3>Orders</h3>
    <ul>
      @for (o of orders(); track o.id) {
        <li>Order #{{ o.id }}: {{ o.total }} USD</li>
      }
    </ul>
  `,
})
export class AccountOrders {
  // Filled by the resolver with the same name.
  readonly orders = input<Order[]>([]);
}

A resolver loads data before the page opens

// src/app/routing/orders.resolver.ts
import { ResolveFn } from '@angular/router';
import { Order } from './account-pages';

export const ordersResolver: ResolveFn<Order[]> = async () => {
  // Replace with an HttpClient call. The page opens after this finishes.
  await new Promise((resolve) => setTimeout(resolve, 100));
  return [
    { id: 101, total: 25 },
    { id: 102, total: 80 },
  ];
};

Wire it up

// src/app/routing/account.routes.ts
import { Routes } from '@angular/router';
import { AccountLayout } from './account-layout';
import { AccountOrders, AccountProfile } from './account-pages';
import { ordersResolver } from './orders.resolver';

export const ACCOUNT_ROUTES: Routes = [
  {
    path: '',
    component: AccountLayout,
    children: [
      { path: '', pathMatch: 'full', redirectTo: 'profile' },
      { path: 'profile', component: AccountProfile, title: 'Profile', data: { section: 'Account' } },
      { path: 'orders', component: AccountOrders, title: 'Orders', resolve: { orders: ordersResolver } },
    ],
  },
];
// src/app/app.routes.ts
import { inject } from '@angular/core';
import { CanMatchFn, Routes } from '@angular/router';
import { Auth } from './auth/auth';

// canMatch decides whether a route is even considered, so lazy code is not
// downloaded for people who cannot use it.
const loggedIn: CanMatchFn = () => inject(Auth).isLoggedIn();

export const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'account' },
  {
    path: 'account',
    canMatch: [loggedIn],
    loadChildren: () => import('./routing/account.routes').then((m) => m.ACCOUNT_ROUTES),
  },
  {
    path: 'account',
    loadComponent: () => import('./auth/login').then((m) => m.Login),
    title: 'Sign in',
  },
  { path: '**', redirectTo: 'account' },
];

When the user is not logged in, the first account route does not match, so Angular tries the next one with the same path and shows the login page instead. The account chunk is never downloaded.

Preloading and view transitions

// src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import {
  PreloadAllModules,
  provideRouter,
  withComponentInputBinding,
  withPreloading,
  withViewTransitions,
} from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(
      routes,
      withComponentInputBinding(), // route params, data and resolved data become inputs
      withPreloading(PreloadAllModules), // download lazy chunks in the background
      withViewTransitions(), // use the browser View Transitions API when available
    ),
    provideHttpClient(withFetch()),
  ],
};
// src/app/app.ts
import { Component, inject } from '@angular/core';
import { Router, RouterLink, RouterOutlet } from '@angular/router';
import { Auth } from './auth/auth';

@Component({
  imports: [RouterOutlet, RouterLink],
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
})
export class App {
  private readonly auth = inject(Auth);
  private readonly router = inject(Router);

  protected signIn(): void {
    this.auth.login('Demo user');
    // Go through '/' so the router matches the routes again, now that we are signed in.
    this.router.navigateByUrl('/');
  }
}
<!-- src/app/app.html -->
<header>
  <a routerLink="/account">Account</a>
  <button type="button" (click)="signIn()">Quick sign in (demo)</button>
</header>
<main>
  <router-outlet />
</main>
  • PreloadAllModules is simple. For big apps write a custom PreloadingStrategy that preloads only routes marked with data: { preload: true }.
  • withViewTransitions() gives a smooth cross-fade between pages in browsers that support it, and does nothing elsewhere.

Router events

Listen to navigation for analytics or a progress bar:

// src/app/routing/navigation-tracker.ts
import { Injectable, inject } from '@angular/core';
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
import { signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class NavigationTracker {
  private readonly router = inject(Router);
  readonly navigating = signal(false);

  constructor() {
    this.router.events.subscribe((event) => {
      if (event instanceof NavigationStart) {
        this.navigating.set(true);
      } else if (event instanceof NavigationEnd) {
        this.navigating.set(false);
        console.log('Page view', event.urlAfterRedirects);
      }
    });
  }
}

This service lives for the whole app, so the subscription is fine. In a component, add takeUntilDestroyed().

The key parts

  • Child routes with a layout component remove repeated navigation markup.
  • Resolvers run before the page opens. Use them when the page makes no sense without data, and keep them fast, or show a loading bar using navigation events.
  • `data` attaches static values to a route, and withComponentInputBinding() delivers them as inputs.
  • `canMatch` stops a route from matching at all, which is the right choice for lazy routes and feature flags.
  • Preloading trades a little background bandwidth for instant navigation later.

Try it yourself

  1. Add a third child route settings and a link to it.
  2. Show a progress bar using NavigationTracker.navigating.
  3. Write a custom preloading strategy that preloads only routes with data: { preload: true }.

Quick check

  1. What is the difference between canActivate and canMatch?
  2. When should you use a resolver?
  3. How do resolved values reach the component?

Next, understand when Angular updates the screen, and how to go zoneless, in change detection and performance. See the routing guide.

Frequently asked questions

Should I use resolvers or load data in the component?

Loading in the component with a loading state feels faster for users. Use resolvers when the page cannot render without the data or when you want a redirect on failure.

Do view transitions work in all browsers?

They need browser support for the View Transitions API. In other browsers navigation works normally without the animation.