January 21, 2025
Top 8 New Angular Features in Recent Versions
The competition between frontend and UI development libraries and frameworks has never been so intense. Every library/framework keeps introducing new features not only to make it possible to develop more performant web applications but also to make the development experience more intuitive for developers.
As a result, even though my most favorite library of all time React and its amazing meta-framework Next.js has been improving with incredible new features like server components, and has exceeded other frameworks in popularity a long time ago, when it comes to user experience and choosing the right technology for specific business use cases, you can’t be biased about frameworks and libraries! The competitors haven’t been idle either. New, amazing libraries like Solid and Qwik have emerged, and Vue, Svelte, and Angular have been introducing new features.
Speaking of improvements, in particular, with new features and positive changes in recent versions like 18 and 19, Angular has become a completely different framework since its AngularJS days, and even Angular 2 when I first started using it many years ago. Actually, what Angular has been doing is nothing short of a renaissance. In this article, we’ll learn about some of the top changes made to Angular in recent updates that improve both the framework’s performance and developer experience.
1. Standalone Components by Default
Standalone components are undoubtedly the most prominent feature in Angular introduced in recent years. Added in Angular 14, they allow developers to create components without the need to declare them in an Angular module, which simplifies the component architecture. This approach enables greater flexibility and modularity, as standalone components can be easily reused across different parts of an application without being tied to a specific module. This makes Angular components more flexible, somewhat similar to React. For example, a standalone component can be defined as follows:
import { Component } from '@angular/core';
@Component({
selector: 'app-standalone',
template: `<h1>Hello from Standalone Component!</h1>`,
standalone: true // not needed since Angular 19
})
export class StandaloneComponent {}
Unlike traditional Angular modules, which require components, directives, and services to be declared within an NgModule, standalone components encapsulate their dependencies, making them self-contained. Developers can directly import and use these components in other parts of their application or even in different projects without the overhead of managing module dependencies.
Now in Angular 19 and beyond, components are standalone by default, and you don’t need to declare “standalone: true” anymore!
2. Signals
Even though they already existed in earlier years under different names, signals in JavaScript a concept inspired by reactive programming—were first introduced in the context of the JavaScript language with the advent of frameworks like Svelte and Solid.js around 2020. These frameworks utilize signals as a way to manage state and reactivity more efficiently, creating highly responsive applications without the overhead of traditional state management techniques. As a result, Angular adopted them as well in v17.
A signal is essentially a wrapper around a value that notifies interested consumers when that value changes. Signals can contain any value, from primitives to complex data structures. We can declare, get, set, and update them like this:
import { Component, signal } from '@angular/core';
const count = signal<number>(0);
// setting
this.count.set(1);
// updating
this.count.update(current => current + 1);
// getting
console.log(this.count());
3. Computed
The main advantage of signals over just using variables is that we can get notified when the signal value changes and then respond to the new value. Computed signals are derived from other signals using a derivation function. They allow you to create dynamic values based on existing signals. Computed signals cannot be assigned values directly. Attempting to set a value for a computed signal will result in a compilation error. Here’s how to create a computed signal:
import { Component, signal, computed } from '@angular/core';
count = signal<number>(0);
// Create a computed signal for ‘doubleCount’
doubleCount = computed(() => {
return this.count() * 2;
});
4. Linked Signal
Introduced recently in Angular 19, a linked signal is a new feature to help you manage state that automatically syncs with other signals. In simple terms, you receive a writable signal that resets itself when the value of its source signal changes. A linked signal can be created using the linkedSignal()
factory function. Unlike computed signals, it stays writable, allowing us to override it when needed:
import { Component, signal, linkedSignal } from '@angular/core';
const timestamp = signal(Date.now());
const timestampSeconds = linkedSignal(() => timestamp() / 1000);
timestampSeconds.set(0); // we couldn’t set it if it was computed instead of linkedSignal
5. Template @ Blocks
Angular template syntax introduced the @
operators to enhance template expression capabilities, enabling developers to write much more concise and readable Angular templates compared to older ng
directives. Notable among these operators are @if
, @for
, @switch
, and @let
. The @if
directive lets you conditionally render elements, while @for
enables iteration over collections, @switch
provides a cleaner way to handle multiple conditional paths, and @let
allows for defining local variables within the template scope. Here’s a short example demonstrating these operators:
<main>
@let age = user.age;
@if (age > minimumAge) {
{{age}} is greater than {{ minimumAge }}
} @else if (minimumAge > age) {
{{age}} is less than {{ minimumAge }}
} @else {
{{age}} is equal to {{ minimumAge }}
}
@for (item of items; track item.name) {
<li>{{ item.name }}</li>
} @empty {
<li>There are no items.</li>
}
@switch (condition) {
@case (caseA) {
Case A.
}
@case (caseB) {
Case B.
}
@default {
Default case.
}
}
</main>
6. Deferrable Views
Speaking of @
blocks in templates, the new @defer
operator is extremely useful for improving performance. It can be used to defer-load the JavaScript for components, directives, and pipes used inside a component template. Critical resources should be loaded first, while other less crucial resources should be loaded later, only when and if needed. @defer
helps us achieve this in Angular:
@Component({
selector: 'app',
template: `
<h2>Some content that will always be displayed ...</h2>
@defer {
<large-component />
}
`,
})
export class AppComponent {}
7. Resource API
The Resource
API is another interesting and handy feature introduced recently in v19. It aims to facilitate handling asynchronous requests, similar to what React is trying to achieve with “Suspense” and “use” or what’s possible with React Query. The new resource primitive in Angular provides an elegant way to manage asynchronous API calls in a signal-based application. By leveraging resource
, you can seamlessly integrate reactive API calls into your app with better state handling and cleaner code:
import { Component, signal, resource } from '@angular/core';
@Component({
selector: 'my-component',
template: `
<input (input)="search($event)" placeholder="Search user..."/>
<br />
<ul>
@let error = users.error();
@if (error) {
<p>{{ error }}</p>
}
@if (users.isLoading()) {
<p>Loading Users...</p>
}
@for (user of users.value(); track user.id) {
<li>{{ user.firstName }} {{ user.lastName }}</li>
} @empty {
<p>No Users!</p>
}
</ul>
`,
})
export default class MyComponent {
query = signal<string>('');
users = resource<User[], string>({
request: () => this.query(),
loader: async ({ request, abortSignal }) => {
const response = await fetch(`${API_URL}/search?q=${request}`, {
signal: abortSignal,
});
if (!response.ok) throw new Error('Unable to load users!');
return (await response.json()).users;
},
});
search(event: Event) {
const { value } = event.target as HTMLInputElement;
this.query.set(value);
}
}
8. Incremental Hydration
Incremental hydration is a performance improvement that builds on top of full application hydration. It can produce smaller initial bundles while still providing an end-user experience that is comparable to a fully hydrated application. Smaller bundles improve initial load times, reducing First Input Delay (FID) and Cumulative Layout Shift (CLS). You can enable incremental hydration for applications that already use server-side rendering (SSR) with hydration. Here’s how to enable it in your project:
import {
bootstrapApplication,
provideClientHydration,
withIncrementalHydration,
} from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [provideClientHydration(withIncrementalHydration())],
});
With incremental hydration, you can add additional triggers to @defer
blocks that define incremental hydration boundaries. Adding a hydrate trigger to a defer block tells Angular that it should load that defer block’s dependencies during server-side rendering.
In this article, I reviewed some of the most prominent and useful changes and new features of Angular. I recommend staying tuned with the latest updates in this framework to deliver the best user experience and performance.