Angular v20

2 days ago 5

Minko Gechev

The past couple of years have been transformative for Angular, as we’ve unleashed major advancements like reactivity with Signals and the power of Zoneless applications. We hope these features have helped the Angular community build the next generation of web applications with fast time-to-market and robust performance.

A banner image showing the text “Angular v20” with the Angular logo in the front. Under this, there’s the number 20 in a animated construction site with a skyline.

And we are just getting started! Angular v20 is our latest release where we have spent countless hours polishing some of our in-progress features for the rock-solid developer experience that you deserve.

Some of the highlights:

  • Stabilizing APIs such as effect, linkedSignal, toSignal, incremental hydration, route-level render mode config and promoting zoneless to developer preview
  • Improved debugging with Angular DevTools and partnering with Chrome for custom Angular reporting directly in Chrome DevTools
  • Polishing developer experience with style guide updates, type checking and language service support for host bindings, support for untagged template literal expressions in templates, template hot module replacement by default, and more.
  • Advancements in GenAI development with llms.txt and angular.dev guides and videos for building Generative AI applications
  • Launching a request for comments for an official mascot for Angular

We spent the past three years rethinking Angular’s reactivity model to make it more robust and future-proof. In Angular v16 we shipped a developer preview of Angular Signals and since then, they got a wide adoption in and outside of Google.

YouTube shared on stage how using Angular Signals with Wiz, they improved input latency in Living Room with 35%. In the meantime, TC39 kicked off an investigation to introduce Signals to the JavaScript language with reference implementation based on Angular Signals.

After collecting feedback from RFCs and iterating on the implementation, we promoted signal, computed, input and view queries APIs to stable. Today, we are announcing effect, linkedSignal and toSignal as stable as well.

To tackle managing asynchronous state with Angular, in v19 we developed the resource API. Since then, we introduced resource streaming and created a new API called httpResource that allows you to make HTTP requests with a Signal-based reactive API. Both of these APIs are available as part of v20 as experimental.

The resource API allows you to initiate an asynchronous action when a signal changes and expose the result of this action as a signal:

const userId: Signal<string> = getUserId();
const userResource = resource({
params: () => ({id: userId()}),
loader: ({request, abortSignal}): Promise<User> => {
// fetch cancels any outstanding HTTP requests when the given `AbortSignal`
// indicates that the request has been aborted.
return fetch(`users/${request.id}`, {signal: abortSignal});
},
});

The code above will fetch the user with the particular identifier, when the userId signal changes.

Now let’s suppose we’re getting data from a WebSocket. For this purpose we can use a streaming resource:

@Component({
template: `{{ dataStream.value() }}`
})
export class App {
// WebSocket initialization logic will live here...
// ...
// Initialization of the streaming resource
dataStream = resource({
stream: () => {
return new Promise<Signal<ResourceStreamItem<string[]>>>((resolve) => {
const resourceResult = signal<{ value: string[] }>({
value: [],
});

this.socket.onmessage = event => {
resourceResult.update(current => ({
value: [...current.value, event.data]
});
};

resolve(resourceResult);
});
},
});
}

In this minimal example, we declare a new streaming resource, which returns a promise of a signal. The signal has a value type ResourceStreamItem<string[]>, which means that the signal can hold the value { value: string[] } or {error: … } in case we want to return an error.

We emit the values we receive over the WebSocket via the resourceResult signal.

Building on top of this pattern, we also shipped the experimental httpResource:

@Component({
template: `{{ userResource.value() | json }}`
})
class UserProfile {
userId = signal(1);
userResource = httpResource<User>(() =>
`https://example.com/v1/users/${this.userId()}`
});
}

The snippet above will send an HTTP GET request to the URL we specified every time when the userId changes. httpResource returns HttpResourceRef which has a value property of type signal that we can directly access in the template. The userResource has other values, such as isLoading, headers, and others, as well.

Under the hood, httpResource uses HttpClient so you can specify interceptors in the HttpClient provider:

bootstrapApplication(AppComponent, {providers: [
provideHttpClient(
withInterceptors([loggingInterceptor, cachingInterceptor]),
)
]});

Over the past six months, we made a lot of progress in zoneless, specifically around server-side rendering and error handling.

Many developers use Zone.js for capturing errors in their apps even without realizing it. Zone.js also lets the framework know when we’re ready to flush the server-side rendered application to the client. In the world of zoneless, we had to find robust solutions for these problems.

In v20 we now have a default handler for unhandledRejection and uncaughtException in Node.js during SSR to prevent the node server from crashing in case of errors.

On the client, you can include provideBrowserGlobalErrorListeners in your providers. You can start using zoneless today by updating your list of providers:

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

In addition, make sure you remove the zone.js polyfill from your angular.json. Learn more about the benefits of zoneless and how to transition your project in our documentation.

If you’re creating a new Angular project, you can get it to be zoneless from the start using the CLI:

A gif showing the execution of `ng new`. One of the questions Angular CLI asks is if the developer who ran `ng new` wants to create a zoneless app in developer preview.

In v20 we also focused on polishing our flagship server-side rendering features — incremental hydration and route-level rendering mode configuration. Today, we’re happy to promote both of them to stable!

As a reminder, incremental hydration makes your apps faster by downloading and hydrating a portion of the page on a particular trigger. This way, your users don’t have to download all the JavaScript associated with a particular page, instead they can gradually download only the parts that they need.

To start using incremental hydration today, configure hydration by specifying withIncrementalHydration:

import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';

// ...
provideClientHydration(withIncrementalHydration());

In the templates of your components now you can use deferrable views:

@defer (hydrate on viewport) {
<shopping-cart/>
}

That way, Angular will download the shopping cart component together with its transitive dependencies and hydrate this part of the UI only when it enters the viewport.

Additionally, you can now use route-level rendering mode configuration as a stable API! If different routes in your app have different rendering requirements, you can configure that in a server-route configuration:

export const routeConfig: ServerRoute = [
{ path: '/login', mode: RenderMode.Server },
{ path: '/dashboard', mode: RenderMode.Client },
{
path: '/product/:id',
mode: RenderMode.Prerender,
async getPrerenderParams() {
const dataService = inject(ProductService);
const ids = await dataService.getIds(); // ["1", "2", "3"]
// `id` is used in place of `:id` in the route path.
return ids.map(id => ({ id }));
}
}
];

In the snippet above we configure to render the login page on the server, the dashboard on the client, and prerender the product pages.

Notice that the product page requires an id parameter. To resolve the identifiers for each product, we can use the asynchronous function getPrerenderParams. It returns an object in which its keys map to router parameters. In the case of the /product/:id page we return an object with an id property.

You can host your server-side rendered apps with most cloud providers. We partnered closely with Firebase App Hosting on a seamless deployment story that supports hybrid rendering (SSR, SSG, and CSR) and provides you the security and scalability of Google Cloud.

We spent a lot of time while developing v20 on engineering excellence — polishing existing APIs to improve your developer experience. We did this across the board — framework, router, forms, http, etc. Let me share more about the work we did here!

Performance insights in Chrome DevTools

To further enhance the developer experience and provide deeper insights into application performance, we’ve collaborated with the Chrome DevTools team to integrate Angular-specific profiling data directly into the Performance panel. Previously, developers often had to switch between framework-specific profilers and the browser’s DevTools, making it challenging to correlate information and pinpoint bottlenecks, especially with minified production code. This new integration aims to solve that by displaying Angular runtime data, such as component rendering, change detection cycles, and event listener execution, within the same timeline as other browser performance metrics.

This direct integration, available starting in Angular v20, leverages the Performance panel extensibility API, specifically using the console.timeStamp API for its low overhead, ensuring that profiling doesn’t negatively impact application performance. Developers can now gain enhanced visibility into Angular’s internal workings, with color-coded entries to distinguish between developer-authored TypeScript code and Angular compiler-generated code. To enable this feature, simply run the global utility ng.enableProfiling() in your application or the DevTools console. This advancement provides a more intuitive and comprehensive performance analysis experience, empowering developers to build even more performant Angular applications.

Screenshot of Chrome DevTools. The image shows the “Performance” tab in which there’s an Angular-specific information, such as, how long did it take to create a particular component and how long we spent in change detection.

On the screenshot above, you can see this feature in action. Notice how at the bottom of the performance timeline there’s a track dedicated to Angular. With the color-coded bars, you can preview component instantiation, running change detection, etc. Both Angular DevTools and the Angular track in the Chrome performance timeline use the same hooks with the difference that Chrome’s performance timeline can put your app’s lifecycle into the context of other JavaScript calls outside the framework.

In addition, the Angular track in Chrome’s performance timeline shows some data that’s currently not present in Angular DevTools, such as component and provider instantiation.

Framework additions and improvements

To dynamically create an Angular component you can use the createComponent function. In v20 we introduce new features that let you apply directives and specify bindings to dynamically created components:

import {createComponent, signal, inputBinding, outputBinding} from '@angular/core';

const canClose = signal(false);
const title = signal('My dialog title');

// Create MyDialog
createComponent(MyDialog, {
bindings: [
// Bind a signal to the `canClose` input.
inputBinding('canClose', canClose),

// Listen for the `onClose` event specifically on the dialog.
outputBinding<Result>('onClose', result => console.log(result)),

// Creates two way binding with the title property
twoWayBinding('title', title),
],
directives: [
// Apply the `FocusTrap` directive to `MyDialog` without any bindings.
FocusTrap,

// Apply the `HasColor` directive to `MyDialog` and bind the `red` value to its `color` input.
// The callback to `inputBinding` is invoked on each change detection.
{
type: HasColor,
bindings: [inputBinding('color', () => 'red')]
}
]
});

Above we create a dialog component and specify:

  • canClose input binding, passing the signal canClose as a value
  • Set the output onClose to a callback that logs the emitted result
  • Two way binding between the title property and the title signal

Additionally, we add the FocusTrap and HasColor directives to the component. Notice that we can also specify input bindings for the HasColor directive that we apply to MyDialog.

Extended template expression syntax

We’ve been bridging the gap between Angular template expressions and full JavaScript syntax to enable higher expressiveness and better developer experience. Today, we’re introducing support for the exponential operator ** and in operator:

<!-- n on power two -->
{{ n ** 2 }}

<!-- checks if the person object contains the name property -->
{{ name in person }}

In v20 we also enable you to use untagged template literals directly in expressions:

<div [class]="`layout col-${colWidth}`"></div>

Extended diagnostics

To guard against common errors, we introduced static checks that detect invalid nullish coalescing, detection of missing imports for structural directives, and a warning when you don’t invoke the track function you’ve passed to @for:

@Component({
template: `
@for (user of users; track trackFn) {
<!-- ... ->
}
`
})
class UserList {
users = getUsers();

trackFn() {
// ... body
}
}

The @for loop in Angular templates accepts a track expression. In practice, trackFn by itself is an expression which returns the trackFn function which is a valid value. At the same time, most likely we’d have wanted to call trackFn and the new diagnostics makes it easier to catch such mistakes.

Style guide updates

After seeing how thousands of apps use Angular over the past decade we decided to update our style guide. Our main goals are to modernize it and remove the unnecessary complexities.

After we collected feedback from an RFC, we introduced a series of simplifications — removing non-Angular specific code health practices from the style guide and moving Angular best practices that are unrelated to coding style to the documentation. We also made filename and class name suffixes optional to encourage more intentional naming of the abstractions which reduces boilerplate.

Starting in Angular v20, by default Angular CLI will not generate suffixes for your components, directives, services, and pipes. For existing projects, ng update will enable suffix generation by updating your angular.json. To enable suffix generation in new projects, use the following schematic configuration:

{
"projects": {
"app": {
...
"schematics": {
"@schematics/angular:component": { "type": "component" },
"@schematics/angular:directive": { "type": "directive" },
"@schematics/angular:service": { "type": "service" },
"@schematics/angular:guard": { "typeSeparator": "." },
"@schematics/angular:interceptor": { "typeSeparator": "." },
"@schematics/angular:module": { "typeSeparator": "." },
"@schematics/angular:pipe": { "typeSeparator": "." },
"@schematics/angular:resolver": { "typeSeparator": "." }
},
...
}

Angular has evolved a lot over the years and we wanted to reflect its evolution in the style guide as well. As a result, we removed most guidance related to NgModules and revisited the usage of @HostBinding and @HostListener in favor of the host object within the directive metadata. To ensure we don’t regress developer experience with the new guidelines, we also addressed a couple of gaps in the host binding support.

Improved host bindings

A reason why historically we’ve been recommending @HostBinding and @HostListener was that they had marginally better editor support than the host object in component metadata since you can use them right on a specific binding or a method. At the same time, they could be hard to spot, use decorators, and could lead to more cumbersome code.

In Angular v20 we’re introducing type checking and language support for host binding and listener expressions.

In the gif below you can see this feature in action. We first get an error because we call a function named getAppTile instead of getAppTitle. Once we fix this problem, the language service detects that the program doesn’t type check since we’re passing an argument to a function that doesn’t expect any arguments.

Gif that shows the enhanced language service support for host bindings. The developer puts their cursor over a host binding that says `getAppTile` and we get the error that’s not an existing method. The developer changes the method name to `getAppTitle`. The language service shows an error this method does not accept any parameters, so the developer removes the parameter.

To enable this feature, set the typeCheckHostBindings property under angularCompilerOptions in tsconfig.json to true. We’ll enable this feature by default in Angular v21.

Incremental hydration in Angular DevTools

To simplify debugging of incremental hydration and deferrable views, you can now preview them in Angular DevTools directly!

The screen capture below shows how you can inspect a defer block and the content that it later loads.

Gif showing Angular DevTools. The developer explores the component tree of the app and sees an entry of `@defer` block. They scroll the app down a little, which triggers the download of the deferred components.

When using defer blocks with incremental hydration, you’d also get icons indicating whether Angular hydrated the current component yet.

Experimental support for vitest

With the deprecation of Karma, we worked with testing framework authors to find a well maintained replacement that enables browser testing. We landed a pull request that creates an experimental playground for us to try different test runners.

In v20, Angular CLI comes in with an experimental vitest support that has watch mode and browser testing!

To give vitest a try in node environment, within your project run:

npm i vitest jsdom --save-dev

After that update your testing configuration in angular.json to:

"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
"buildTarget": "::development",
"runner": "vitest"
}
}

Next you may have to update your unit test files to include correct imports:

...
import { describe, beforeEach, it, expect } from 'vitest';
...

Finally, ng test to run your unit tests with vitest.

Quality of life improvements in Angular Material

In this release we polished further our button component to better align with the M3 specification.

 “Tonal”, “Basic”, “Disabled”, and “Link.”

Tonal button

A few of the changes:

  • We implemented tonal button
  • Aligned terminology with M3 specification
  • Added the ability to set the default appearance for buttons
  • Added matIconButton selector to the icon button for consistency

A few quality of life improvements that we implemented include:

  • New closePredicate for dialog which closes a request with 108 👍
  • New overlay APIs for better tree-shaking
  • Now handling `prefers-reduced-motion` automatically
  • New DI token to disable animations
  • MatButton and MatAnchor are combined so users don’t have to import both of them.

Supporting developers utilizing GenAI

To enable LLMs produce modern Angular code and enable you to build apps with GenAI we kicked off two efforts

  • Maintaining an llms.txt file (see the pull request on GitHub) that helps large language models discover the latest Angular documentation and code samples
  • Providing starting point for developers who are building apps using GenAI

Some language models still produce older Angular syntax using structural directives instead of the latest control flow, or using NgModules instead of standalone components, directives, and pipes. Resolving this problem is a multi step process that we started with creating an llms.txt file. In the future, we’ll continue providing code samples using the latest Angular syntax and explore the development of system prompts that hint LLMs to use the correct APIs.

The second effort we started is to provide guidelines for developers who are building APIs with AI features. We ran multiple live streams showcasing how you can leverage Genkit and Vertex AI in your Angular app. We open sourced the sample apps and listed some of the best practices we discovered on angular.dev/ai.

That’s just the very beginning of making Angular the solution for your agentic AI apps.

We introduced Angular’s built-in control flow back in v17 to bring a series of improvements:

  • More intuitive syntax, that’s closer to JavaScript
  • Simpler use, without need to import another module or individual directives
  • Performance improvement by updating the diffing algorithm
  • Improved type checking via type narrowing

We also shipped a schematic that with a single line of code allows you to move your projects from structural directives to the built-in control flow:

ng generate @angular/core:control-flow

Currently, more than half of the Angular v17+ apps on the HTTP Archive public data set use the new syntax!

Based on the community sentiment and adoption metrics, moving forward we are deprecating *ngIf, *ngFor, and *ngSwitch and encouraging everyone to use the latest built-in control flow. This means that under our deprecation policy we can remove structural directives in version 22, one year from now.

As Angular continues to grow and evolve, we’re excited to announce a new initiative that will further enrich our amazing community: the creation of an official Angular mascot! While Angular is a well-recognized and widely adopted framework, it has been missing a fun, visual representation that so many other successful open-source projects enjoy. We’ve heard your requests for something tangible to connect with, like a plushy or keychain, and we’re thrilled to embark on this creative journey with all of you. Our team collaborated with the designers behind the Dart and Firebase mascots, sharing Angular’s core strengths and community spirit. This process led to three initial mascot proposals that we’re eager to share.

This is where you, the Angular community, come in! True to Angular’s values of inclusivity and community-driven decisions, we’re opening up the process for everyone to contribute. Find the official Angular mascot RFC at goo.gle/angular-mascot-rfc.

Here’s a sneak peak of the initial concepts:

A mascot character inspired by the Angular logo. It’s with a shield-line shape with feet and hands. It’s smiling.

“Angular shaped character” drawing inspiration from our logo

Angular mascot character. It’s an animated anglerfish.

Wise and resilient “Anglerfish” (much cuter than its real-life counterpart!)

Angular mascot character. It’s an animated anglerfish, similar to the previous one, but with more square shape and two angular teeth.

A variation of the Anglerfish.

We invite you to check out the full RFC, cast your vote for one of these proposals, suggest improvements, and even propose names. Your feedback has been invaluable in shaping Angular’s features, and we’re excited to see that same collaborative spirit define our future mascot. Let’s create something truly special together!

Around the world there are thousands of library authors, conference and meetup organizers, educators, and other people who are helping move the web forward through Angular! We’d never been here without you all.

Since the release date of v19, we received and merged commits from over 225 people across the framework, components, and CLI! Every single change that you made helps us make Angular better. I wanted to highlight some of the features we got from community members:

Thank you all 🙏

As part of v20 we shipped a lot of polishing touches to the large efforts we kicked off over the past couple of years such as reactivity, zoneless, incremental hydration, framework and forms APIs. We also put initial high-level sketches for upcoming advancements such as selectorless, signal-forms, unit testing, and an official Angular mascot!

We’re building Angular for you and your input is critical in how we move forward with any of these initiatives. As our high-level plans for these major projects take shape, we’ll share updates and requests for comments. Until now, make sure you share your thoughts for the future, official Angular mascot! We’re very excited to find an identity that symbolizes Angular’s product and values. Let us know what you think about the concepts in the RFC!

Until next time and thank you for being part of Angular’s journey 🙏

Read Entire Article