🎯Key updates and novel capabilities

Angular 16 arrives with a set of significant updates. The following list covers the most notable additions and changes:

  • Signals
  • DestroyRef
  • takeUntilDestroyed
  • Required inputs
  • Routing data bound to component inputs
  • Node.js v14 support has been dropped
  • TypeScript 5.0 Support
  • The Angular Compatibility Compiler (ngcc) has been removed
  • Server-side rendering now includes non-destructive hydration, allowing the client to reuse server-side request results
  • Enhanced strict type checking for ngTemplateOutlet
  • The provideServiceWorker function for registering service workers in standalone apps
  • Experimental Jest support

Additionally, Angular 16 introduces a new development server built on Vite and Esbuild.

While reading articles on these changes is useful, you should also consult:

for deeper insights.


📌Signals

Official docs: Angular Signals
GitHub discussion: Angular Reactivity with Signals
RFC: RFC: Angular Signals
PR: feat(core): add Angular Signals to the public API
PR: Prototype of the RxJS interop layer for signals
PR: feat(core): Mark components for check if they read a Signal

Angular introduces a new primitive type called a "Signal". This type stores a value much like a standard variable, but it also informs any interested consumers whenever that value changes. Signals can hold both primitive data types and objects.

A "Computed Signal" derives its value from other Signals. To create one, you use the computed function and provide a derivation function that specifies how the value is calculated.

If a component uses the OnPush change detection strategy and includes a Signal in its template, Angular tracks that Signal as a dependency for the component. When the Signal's value changes, Angular marks the component for check. This behavior will evolve in Angular 17, which will support Signal-based per-component change detection without requiring zone.js.

For an introduction to Signals and an overview of what's new in Angular 16 and what's planned for Angular 17, check out Pawel Kozlowski’s talk at NgBe:

Deborah Kurata has authored a comprehensive tutorial on Signals. Her article covers the rationale for Signals, explains what they are, and walks through the process of creating, reading, and updating them. She also demonstrates computed Signals and effects.

Manfred Steyer provides a comparison between the current Zone.js-based change detection and the new Signal-based approach.

For details on how Signals and RxJS work together, Stefan Haas explains Signal and RxJS interop.

If you want to understand the mechanics of Signals using the push & pull model, Tomas Trajan has published an explanation that covers these concepts.


📌DestroyRef

Official docs: DestroyRef
Commit: feat(core): introduce concept of DestroyRef

DestroyRef provides a way to register callbacks that execute for any cleanup or destruction behavior.

Example:

@Component({
  selector: 'component-1',
  standalone: true,
  template: ``,
})
class Component1 {
  constructor(private destroyRef: DestroyRef) {
    destroyRef.onDestroy(() => {
      // run this cleanup code, when the component is destoryed
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

In a follow-up article, Netanel Basal demonstrates how to build a reusable cleanup routine that runs when a given scope is destroyed.


📌takeUntilDestroyed

Commit: feat(core): implement takeUntilDestroyed in rxjs-interop

The takeUntilDestroyed operator completes an observable when the associated component, directive, service, or pipe is torn down.

Consider this example: when the component initializes, it subscribes to a 2-second interval() and logs 'Tick' messages to the browser console. When the component gets destroyed, it automatically unsubscribes from that interval():

@Component({
  selector: 'component-1',
  standalone: true,
  template: ``,
})
class Component1 {
  constructor() {
    interval(2000).pipe(takeUntilDestroyed()).subscribe(() => console.log('Tick'));
  }
}
Enter fullscreen mode Exit fullscreen mode

You can also provide a specific DestroyRef as an argument to takeUntilDestroyed, which allows you to build reusable functions that clean up their subscriptions when the relevant context is destroyed:

@Component({
  selector: 'component-1',
  standalone: true,
  template: ``,
})
class Component1 {
  constructor(private destroyRef: DestroyRef) {
    reusableTicks(destroyRef);
  }
}
Enter fullscreen mode Exit fullscreen mode

📌Required inputs

Official docs: Input
PR: feat(compiler): add support for compile-time required inputs

It is now possible to declare component or directive inputs as required. The snippet below will cause a compile-time error because the template for Component2 does not provide the required text input:

@Component({
  selector: 'component-1',
  standalone: true,
  template: ``,
})
class Component1 {
  // ❗ this is a required input
  @Input({ required: true }) text: string;
}

@Component({
  selector: 'component-2',
  standalone: true,
  template: `
    <component-1></component-1>
  `,
  imports: [Component1]
})
class Component2 {
  constructor(private destroyRef: DestroyRef) {
    destroyRef.onDestroy(() => {
      // run this cleanup code, when the component is destoryed
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

📌Binding Router data to component inputs

Official docs: Router / Getting route information, withComponentInputBinding
PR: Feature to bind Router information to component inputs

This new capability makes it much easier to access route information within components. Enea Jahollari explains the feature through practical examples in a dedicated article.


📌Removal of Node.js v14 support

Changelog entry: Node.js v14 support has been removed

Node.js v14 is scheduled for End-of-Life on 2023–04–30. Angular v16 will no longer support this version, but will continue to officially support Node.js v16 and v18.

📌TypeScript 5

For Angular developers, TypeScript 5 brings two particularly relevant additions: the updated decorator implementation and the ability to extend multiple configuration files through tsconfig.json.

Decorators

TypeScript 5 now aligns with the ECMAScript decorator proposal, introducing what are commonly called 'Standard decorators'.

Kevin Kreuzer dives into the mechanics of these new decorators in his article.

Multiple configuration file support in tsconfig.json‘s extends

The extends property in tsconfig.json now accepts an array of paths. In the example below, the resulting configuration has both strict and noImplicitReturns turned on:

// tsconfig-a.json
{
    "compilerOptions": {
        "strict": true
    }
}

// tsconfig2-b.json
{
    "compilerOptions": {
        "noImplicitReturns": true
    }
}

// tsconfig.json
{
    "extends": ["./tsconfig-a.json", "./tsconfig-b.json"],
    "files": [
      "src/main.ts",
      "src/polyfills.ts"
    ]
}
Enter fullscreen mode Exit fullscreen mode

The full list of new TypeScript capabilities is available in their official announcement.


📌Angular Compatibility Compiler (ngcc) has been removed

Changelog entry: Angular Compatibility Compiler (ngcc) has been removed

Angular 9 marked the shift from the “View Engine” to “Ivy” as the default rendering pipeline. To bridge the gap for libraries not yet migrated, the Angular Compatibility Compiler (ngcc) was introduced. Starting with Angular 16, ngcc along with all remnants of the “View Engine” have been purged, meaning libraries built on the old architecture are no longer compatible with v16 and beyond.

For a deeper look at how “View Engine” and “Ivy” differ, check out Maria Korneeva‘s explanation of both rendering engines.


📌NgZone is configurable in bootstrapApplication

Official docs: provideZoneChangeDetection
PR: Add ability to configure NgZone in bootstrapApplication

Angular 16 introduces two changes related to zone configuration:

class CustomZone extends NoopNgZone {}

bootstrapApplication(
  StandaloneCmp, { 
  providers: [
    // provide a custom zone implementation
    { provide: NgZone, useValue: new CustomZone() }
   ]
  }
);
Enter fullscreen mode Exit fullscreen mode
bootstrapApplication(
  StandaloneCmp, { 
  providers: [
    // enable eventCoalescing and runCoalescing
    provideZoneChangeDetection({
      eventCoalescing: true,
      runCoalescing: true
    })
  ]
});
Enter fullscreen mode Exit fullscreen mode

📌Server Side Rendering improvements

Non-destructive hydration

PR: feat(platform-browser): add a public API function to enable non-destructive hydration

With non-destructive hydration, Angular can now reuse the DOM structure generated on the server instead of rebuilding it on the client. This approach eliminates the visual flicker that often appears as the application becomes interactive.

The results of the request done on the server side can be reused on the client side

PR: feat(platform-browser): enable HTTP request caching when using provideClientHydration

To spare the client from re-sending the same network requests, responses obtained during server-side rendering are now cached and made available to the client-side code.

Jessica Janiuk outlines where server-side rendering stands today and where it’s heading in her blog post.


📌Strict type checking for ngTemplateOutlet

PR: fix(common): strict type checking for ngtemplateoutlet

In earlier versions, the context passed to ngTemplateOutlet was typed simply as Object, so the following snippet compiled without any complaints:

interface Context1 {
  prop: number;
}

@component({
  standalone: true,
  imports: [NgTemplateOutlet],
  selector: 'person',
  template: `
    <ng-container
      *ngTemplateOutlet="
        myTemplateRef;
        context: { prop: 1, missingProp: 2 }
      "></ng-container>
  `,
})
export class Component1 {
  templateRef!: TemplateRef<Context1>;
}
Enter fullscreen mode Exit fullscreen mode

Angular 16 now raises a compile-time error for the same code, since missingProp does not exist on Context1. To resolve this, either add the missingProp property to Context1 or wrap the expression with $any(…) inside the template to keep the old behavior.


📌provideServiceWorker function to register service workers in standalone applications

Official docs: Service workers / provideServiceWorker
PR: feat(service-worker): add function to provide service worker

Registering a service worker in a standalone application previously required the awkward importProvidersFrom(ServiceWorkerModule.register(…)) approach.

The newly added provideServiceWorker offers a much cleaner way to accomplish the same task, as shown here:

bootstrapApplication(AppComponent, {
 providers: [
   provideServiceWorker('my-service--worker.js')
 ],
});
Enter fullscreen mode Exit fullscreen mode

📌Experimental Jest support

The Angular Team introduced the first experimental Jest support in Angular 16. Looking ahead to Angular 17, they plan to swap out the deprecated Karma test runner for the Web Test Runner.


➕ Have you seen any other resources I should add to this Angular 16 Study Guide? Please send it to me so that I can feature it in the article!


👨‍💻About the author

I'm Gergely Szerovay, a frontend development chapter lead. Angular is something I genuinely enjoy both teaching and learning. Every day, I take in as much Angular-related material as I can — whether it's articles, podcasts, or conference talks.

I launched the Angular Addict Newsletter to share the best resources I find each month. Whether you're just getting started or you're already deep into Angular, there's something in it for you.

Alongside the newsletter, I run a publication called Angular Addicts. It's a curated list of the resources I find most insightful. If you're interested in contributing as a writer, feel free to reach out.

Let's keep learning Angular together! Subscribe here 🔥

You can also follow me on Medium, Twitter, or LinkedIn for more Angular insights!