Skip to the solution
MediumFrontend9 min read

How do functional route guards and lazy loadComponent work in modern Angular?

947 views

The problem

Question presented to candidate: "We lazy load an admin reports screen with loadComponent and protect it with a functional guard. A security review says the admin JavaScript still downloads for normal users. Walk me through how functional guards and loadComponent interact in modern Angular, and how you would fix this."

What a strong answer should cover:

  • loadComponent points a route at a lazily imported standalone component; the loader runs at most once and its result is cached.
  • A functional guard is a function using inject(); which kind of guard you attach decides when it runs relative to the lazy download.
  • canMatch runs while matching: if it refuses, the route entry is skipped, the next entry with the same path can match (an "upsell" or 404 page), and the lazy component is not downloaded.
  • canActivate runs after matching; for loadComponent the loader ran only after the guard passed in Angular 22.2.3, but for loadChildren the children are loaded before canActivate, because they are needed for matching.
  • Preloading ignores canMatch (only the deprecated canLoad blocks it), so PreloadAllModules would still download the chunk.
  • Route-level providers are visible to the guards and the lazy component: the same instance.

Clarifying questions expected:

  • "Is preloading enabled in this app?"
  • "What should a non-admin see at that URL: a 404, a login page, or an upsell?"

Code / implementation expected: Yes β€” the same check as canMatch on one lazy route and canActivate on another, with loader calls logged.

  • routing
  • guards
  • lazy

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 9 sections

Read the code

AngularSame check as canMatch and canActivate on lazy routes, with loader logs
import { ApplicationConfig, Component, Injectable, inject, signal } from "@angular/core";
import { CanActivateFn, CanMatchFn, Router, RouterOutlet, Routes, provideRouter } from "@angular/router";

@Injectable({ providedIn: "root" })
export class Session {
  readonly isAdmin = signal(false);
}

// Two guards with the same check. Only the kind of guard differs.
const adminCanMatch: CanMatchFn = () => {
  const ok = inject(Session).isAdmin();
  console.log(`  canMatch    -> ${ok}`);
  return ok;
};
const adminCanActivate: CanActivateFn = () => {
  const ok = inject(Session).isAdmin();
  console.log(`  canActivate -> ${ok}`);
  return ok || inject(Router).parseUrl("/");
};

@Component({ selector: "app-home", template: `<p>Home</p>` })
export class HomeComponent {}
@Component({ selector: "app-upsell", template: `<p>Upgrade to see reports</p>` })
export class UpsellComponent {}
// Stand-ins for files that would really be loaded with import("./...").
@Component({ selector: "app-reports", template: `<p>Admin reports (lazy)</p>` })
export class ReportsComponent {}
@Component({ selector: "app-billing", template: `<p>Admin billing (lazy)</p>` })
export class BillingComponent {}

const lazy = (name: string, cmp: unknown) => () => {
  console.log(`  loadComponent(${name}) called`);
  return Promise.resolve(cmp as typeof ReportsComponent);
};

export const routes: Routes = [
  { path: "", component: HomeComponent },
  // canMatch: if it says no, the router skips this config entry and keeps matching.
  { path: "reports", canMatch: [adminCanMatch], loadComponent: lazy("reports", ReportsComponent) },
  { path: "reports", component: UpsellComponent },
  // canActivate: the route already matched; the guard decides whether to activate it.
  { path: "billing", canActivate: [adminCanActivate], loadComponent: lazy("billing", BillingComponent) },
];

export const appConfig: ApplicationConfig = { providers: [provideRouter(routes)] };

@Component({ selector: "app-root", imports: [RouterOutlet], template: `<router-outlet />` })
export class AppComponent {
  readonly router = inject(Router);
  readonly session = inject(Session);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 23 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

How do you create a custom validator in reactive forms?

Continue