Angular 21 New Features Explained with Real Examples

📅 2026-05-17🏷️ Angular⏱️ 14 min read

Angular 21 Just Made Half of Your Code Obsolete — In a Good Way

If you maintain an Angular codebase older than ~2 years, here's the uncomfortable truth: more than half of the boilerplate you wrote is no longer needed. NgModule imports, *ngIf, BehaviorSubject ladders, ngOnChanges gymnastics, manual subscribe/unsubscribe — Angular 21 quietly turns most of that into a single line of modern code.

This isn't a "deprecate something, rename a flag" release. Angular 21 is the version where signals finish growing up, zone.js becomes optional by default, and the framework stops looking like 2016 React and starts looking like its own thing.

Below is a senior-engineer breakdown of every Angular 21 new feature that actually changes how you build apps — with BEFORE/AFTER code for each, a head-to-head against Angular 20, and a real Angular 21 with .NET Core API example.

📌 Quick Summary — What's New in Angular 21

  • Zoneless change detection is stable and the default for new apps — no more zone.js by default.
  • Signal-based inputs, outputs and queries are stable, with linkedSignal() joining the family.
  • httpResource() — declarative, signal-based HTTP. No more subscribe() + manual loading state.
  • Built-in control flow (@if, @for, @switch) is the standard — *ngIf/*ngFor are legacy.
  • Improved @defer blocks for deferred view loading with cleaner hydration.
  • Full SSG via withRoutes(serverRoutes) and RenderMode.Prerender.
  • Event Replay hydration via withEventReplay() — clicks during hydration don't get dropped.
  • Faster builds with @angular/build:application and TypeScript 5.9.

1. Zoneless Change Detection — zone.js Is Now Opt-In

The pain point: zone.js is ~25 KB of monkey-patched browser APIs. Every setTimeout, every Promise, every addEventListener triggers a global change-detection sweep — even when nothing in your UI changed.

What Angular 21 changes: the provideZonelessChangeDetection() API is stable, and ng new scaffolds projects without zone.js by default. Signals tell Angular exactly when to re-render — nothing else.

BEFORE (Angular 14–17)

// main.ts — zone.js included automatically
import { BrowserModule } from '@angular/platform-browser';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  bootstrap: [AppComponent],
})
export class AppModule {}

AFTER (Angular 21)

// main.ts — no zone.js
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    provideZonelessChangeDetection(),
  ],
});

Why it matters: ~25 KB smaller bundles, dramatically lower change-detection overhead, and predictable rendering. If you've ever debugged "why did everything re-render on a setTimeout", this fixes it forever.

Real-world use case: dashboards with frequent timers (charts polling every second) used to flood the call stack via zone. Zoneless + signals re-renders only the changed component.

2. Signal-Based Inputs, Outputs and Queries — Now Stable

The pain point: the classic @Input() decorator triggers ngOnChanges, which is a string-keyed bag of SimpleChange objects. You'd write the same plumbing in every component.

What Angular 21 changes: signal inputs, outputs and queries are stable and recommended. They're reactive primitives — no lifecycle hook needed to detect a change.

BEFORE

@Component({ selector: 'app-greeting', template: '{{ greeting }}' })
export class GreetingComponent implements OnChanges {
  @Input() name!: string;
  greeting = '';

  ngOnChanges(changes: SimpleChanges) {
    if (changes['name']) {
      this.greeting = `Hi ${this.name}!`;
    }
  }
}

AFTER (Angular 21 signals)

@Component({ selector: 'app-greeting', template: '{{ greeting() }}' })
export class GreetingComponent {
  name     = input.required<string>();
  greeting = computed(() => `Hi ${this.name()}!`);
}

Why it matters: derived state is declared, not procedural. The component is shorter, has no lifecycle hooks, and runs only when name actually changes.

🆕 linkedSignal() — Angular 21 Signals Improvements Example

Sometimes you want a writable signal whose default value is derived from another signal — but the user can still override it. Before, this required effect() hacks. Angular 21 introduces linkedSignal():

const products = signal<Product[]>([]);

// Default-selected product reset to first whenever products list changes,
// but a user click can still override it.
const selectedId = linkedSignal({
  source: products,
  computation: (list, prev) =>
    list.find(p => p.id === prev?.value) ? prev!.value : list[0]?.id,
});

// Set explicitly — overrides the derived default
selectedId.set('product-42');

Real-world use case: a master-detail view. Resetting the selection when the list reloads, while still letting users click around — used to take ~30 lines of RxJS. Now it's 5.

3. Built-in Control Flow Is the Default

The pain point: *ngIf/*ngFor are structural directives — they look like attributes, but they desugar into wrapped templates. The error messages are cryptic, and they pull in CommonModule for no reason.

What Angular 21 changes: @if, @for and @switch are first-class template syntax. They compile to faster code and don't need any imports.

BEFORE

<div *ngIf="isLoggedIn">Welcome back!</div>
<ul>
  <li *ngFor="let user of users; trackBy: trackById">{{ user.name }}</li>
</ul>

AFTER

@if (isLoggedIn()) {
  <div>Welcome back!</div>
} @else {
  <a routerLink="/login">Sign in</a>
}

@for (user of users(); track user.id) {
  <li>{{ user.name }}</li>
} @empty {
  <li>No users yet.</li>
}

Why it matters: built-in @else and @empty (no more "no results" hacks), mandatory track in @for (faster lists by default), and dramatically clearer compile errors. [Read: Angular Standalone Components Migration Guide]

4. httpResource() — HTTP Meets Signals

The pain point: fetching data has always been a manual dance: declare loading flag, call HttpClient, subscribe, set loading false, handle error, unsubscribe in ngOnDestroy. Every component, same pattern, 20+ lines.

What Angular 21 changes: the new httpResource<T>() bundles fetch + loading + error + reactivity into a single signal-based primitive.

BEFORE

@Component({ /* ... */ })
export class UsersComponent implements OnInit, OnDestroy {
  users: User[] = [];
  isLoading = false;
  error?: Error;
  private sub?: Subscription;

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.isLoading = true;
    this.sub = this.http.get<User[]>('/api/users').subscribe({
      next: (u) => { this.users = u; this.isLoading = false; },
      error: (e) => { this.error = e;  this.isLoading = false; },
    });
  }

  ngOnDestroy() { this.sub?.unsubscribe(); }
}

AFTER (Angular 21)

@Component({ /* ... */ })
export class UsersComponent {
  users = httpResource<User[]>(() => '/api/users');
  // users.value() : User[] | undefined
  // users.isLoading() : boolean
  // users.error() : Error | undefined
}

Why it matters: no subscriptions, no leaks, no lifecycle hooks. The fetch URL is itself a reactive function — when an upstream signal changes, the request re-runs automatically.

Reactive URL example:

userId = input.required<string>();
user = httpResource<User>(() => `/api/users/${this.userId()}`);

Change userId, the request fires again. No ngOnChanges, no switchMap.

5. @defer Block Improvements

Defer blocks let you delay loading expensive components until they're actually needed. Angular 21 polishes the triggers and hydration behavior.

@defer (on viewport) {
  <heavy-chart [data]="metrics()" />
} @placeholder (minimum 200ms) {
  <p>Chart will load when you scroll to it</p>
} @loading (after 100ms; minimum 500ms) {
  <app-skeleton />
} @error {
  <p>Couldn't load chart</p>
}

Trigger options: on viewport, on idle, on interaction, on hover, on timer(5s), when condition(). Result: smaller initial bundle, content visible faster, no library needed.

6. SSR & SSG Are Finally Pleasant

Angular's server-side rendering used to be a separate, fragile world. Angular 21 collapses it: one builder, one config, real prerendered HTML for every route.

// app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  { path: 'blog/:slug', renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const { BLOG_POSTS } = await import('./core/data/blogs.data');
      return BLOG_POSTS.map(p => ({ slug: p.slug }));
    },
  },
  { path: '**', renderMode: RenderMode.Prerender },
];

Plus provideClientHydration(withEventReplay()) ensures a click during hydration is queued and replayed — no more "the button didn't work the first time" bug.

📊 Angular 21 vs Angular 20 — What Actually Changed?

The headline differences between Angular 20 and 21:

  • Zoneless: experimental in 20 → stable + default in 21.
  • Signal inputs/outputs/queries: stable in 20 → more polished, faster scheduling in 21.
  • linkedSignal(): didn't exist in 20 → stable in 21.
  • httpResource(): developer preview in 20 → stable in 21.
  • Built-in control flow: opt-in in 20 → default template syntax in 21.
  • SSG with withRoutes(): available in 20 → simpler API + per-route render modes in 21.
  • Build performance: further reduced cold-build time via the application builder.
  • TypeScript: 5.4+ in 20 → 5.9 in 21, with stricter template checks.

In short: 20 was the trial; 21 is the commitment. If you wrote Angular 20 code following modern patterns, your migration to 21 is mostly version bumps and removing experimental flags.

⚡ How Angular 21 Helps in Real Projects (With .NET Core API Example)

Here's an admin dashboard talking to a .NET 9 Web API — the kind of stack most enterprise teams ship. Watch how much shrinks.

The .NET Core 9 API

// UsersController.cs
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserService _users;
    public UsersController(IUserService users) => _users = users;

    [HttpGet]
    public async Task<ActionResult<List<UserDto>>> Get(
        [FromQuery] string? search = null)
    {
        var users = await _users.SearchAsync(search);
        return Ok(users);
    }
}

The Angular 21 Frontend

@Component({
  selector: 'app-users',
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="searchTerm" placeholder="Search users..." />

    @if (users.isLoading()) {
      <p>Loading…</p>
    } @else if (users.error()) {
      <p class="error">{{ users.error()?.message }}</p>
    } @else {
      @for (u of users.value() ?? []; track u.id) {
        <div class="user-row">
          <strong>{{ u.firstName }} {{ u.lastName }}</strong>
          <span>{{ u.email }}</span>
        </div>
      } @empty {
        <p>No users match "{{ searchTerm() }}"</p>
      }
    }
  `,
})
export class UsersComponent {
  searchTerm = signal('');
  users = httpResource<UserDto[]>(() =>
    `https://api.yourapp.com/api/users?search=${encodeURIComponent(this.searchTerm())}`
  );
}

What you DON'T see anymore:

  • No HttpClient injection
  • No subscribe(), no unsubscribe(), no takeUntilDestroyed()
  • No isLoading / error manual state flags
  • No switchMap/debounceTime on a search Subject
  • No ngOnInit, no ngOnDestroy

The reactive URL is enough — type in the input, httpResource() re-fires the request automatically. [Read: JWT Authentication in Angular]

🎯 Angular 21 Interview Questions (With Answers)

Q1. What are the main new features in Angular 21?

Stable zoneless change detection, linkedSignal(), stable httpResource(), signal-based forms refinements, built-in control flow as default, improved @defer blocks, simpler SSG via withRoutes(), TypeScript 5.9.

Q2. What is zoneless change detection and why does it matter?

Zoneless change detection means Angular no longer relies on zone.js to detect when to re-render. Components only update when their reactive primitives (signals, signal inputs, signal-based queries) change. Result: ~25 KB smaller bundles and no global change-detection sweeps on every setTimeout or DOM event.

Q3. Difference between signal() and linkedSignal()?

signal() is a plain reactive container. linkedSignal() is a writable signal whose default value is derived from another signal. When the source updates, the derived default is recomputed, but the user can still override the value explicitly.

Q4. How does httpResource() differ from HttpClient.get()?

httpResource() returns a reactive Resource object exposing value(), isLoading() and error() signals. The fetch URL is itself a reactive function — when its dependencies change, the request automatically re-fires. No subscription management, no leaks.

Q5. How do you migrate *ngIf to @if?

Run ng generate @angular/core:control-flow. The schematic rewrites *ngIf, *ngFor and *ngSwitch to @if, @for and @switch across the project. It also adds the now-required track expression to every @for.

Q6. When would you use @defer?

Anytime a component is expensive (charts, editors, image galleries) and not needed on initial paint. @defer (on viewport) + a skeleton placeholder cuts your initial JS payload without any router-level splitting.

Q7. Can a zoneless Angular 21 app still use third-party libraries that depend on zone.js?

Most pure-Angular libraries are fine. Libraries that rely on zone's automatic change detection (older RxJS-heavy state libraries, some old form helpers) may need NgZone.run() wrappers or migration. Always audit before flipping the switch in a brownfield app.

Q8. What's the role of provideClientHydration(withEventReplay())?

It tells the SSR hydration system to capture user events (clicks, key presses) that happen before JavaScript fully hydrates, then replay them once components are interactive. Prevents the "I clicked but nothing happened" race condition on slow networks.

Q9. How are signal-based inputs different from @Input()?

input() returns a read-only signal — you call it as a function (this.name()) and other signals can depend on it via computed(). You don't need ngOnChanges; updates are reactive. input.required() makes it mandatory at the type level.

Q10. Is Angular 21 backward compatible with Angular 17 code?

Largely yes — Angular has a strong policy of carrying forward APIs across versions. ng update migrates breaking changes via schematics. But the recommended style has shifted: standalone components, signals, control flow. Code from 17 will compile; it just won't look idiomatic.

⚠️ Common Mistakes Developers Make with Angular 21

  • Forgetting () on signals in templates. {{ count }} renders the function itself; you need {{ count() }}.
  • Skipping track in @for. Required in Angular 21. Without a stable track key you'll get unnecessary DOM churn.
  • Calling httpResource() outside an injection context. Either declare it as a class field or use runInInjectionContext() in services.
  • Using effect() to push values into the DOM. Use computed() for derived state; effect() is for side effects (logging, syncing to localStorage), not rendering.
  • Disabling zone.js without auditing third-party libs. Some older libraries assume zone is running — flip zoneless on after a real audit.
  • Mixing linkedSignal() with heavy computations. The computation runs on every source change; keep it cheap.
  • Subscribing to RxJS inside components again. If you reach for subscribe(), ask first whether toSignal() or httpResource() would do.

🧠 So, Should You Upgrade to Angular 21?

Yes, soon, if any of these apply:

  • You're starting a new project (zero reason to start on anything older).
  • You're on Angular 17+ and use mostly first-party Angular libraries.
  • You're hitting change-detection performance ceilings.
  • You want the smallest bundle Angular has ever shipped.

Wait a release or two if:

  • Your app is on Angular 12–14 with heavy NgModule use — do an intermediate jump first.
  • You depend on libraries that haven't published Angular 21-compatible versions yet.
  • You're locked into zone.js-aware third-party widgets you can't replace this quarter.

For most teams, the realistic plan is: upgrade to 21, leave zone.js enabled, migrate to signals incrementally, then flip zoneless once the codebase is signal-native.

💡 Keep Reading

  • Going Zoneless in Production — Tradeoffs, Pitfalls and Real Benchmarks
  • Migrating an Angular App to Signals — A Real Refactor Diary
  • Building a Full-Stack App with Angular 21 + .NET 9 Web API + JWT Authentication

Angular 21 isn't a feature dump — it's a coherent rethink. The day you ship a component without subscribe, ngOnInit, ngOnDestroy, BehaviorSubject, *ngIf or SimpleChanges is the day you realize how much accidental complexity Angular just deleted from your codebase.