Angular has since the latest major version started what the Angular team calls a renaissance, which means a radical renewal of the framework. The main goals of the latest updates have been to improve the developer experience and performance so it aligns more with the other leading front-end frameworks in the space by introducing new features such as signals and standalone components.
This post will cover the latest features of Angular 17.
Signals
Signals was introduced as a developer preview in Angular 16. It is a reactive state management solution for synchronous reactivity with a lower learning curve than RxJS.
Angular 17 comes with more mature upgrades to Signals and it is now ready to be used in production.
Local change detection
One of the main reasons for signals was a more fine-grained way of doing change detection only on the parts of the DOM that should change from the state change. With the traditional ZoneJS change detection, Angular was doing full top-down change detection every time one of the ZoneJS patched events was triggered:
This can be optimized using OnPush
change detection so it performs less change detection:
But still, there is a lot of dirty checking and change detection being performed on unrelated components eg. when interacting with a component and all the parent components have to be dirty changed and performed change detection on.
Obviously, this is not very efficient, especially if you compare it to the other leading frontend frameworks this made Angular look like a dinosaur. So they borrowed Signals from SolidJS and you will since Angular 17 get local change detection when you use Signals in a component with OnPush
change detection (skipping the parent checks).
Now, if you combine OnPush change detection with Signals you can have local change detection for only the affected component:
As said, other components with default change detection will still be checked but the components behind OnPush change detection are not concerned. Therefore, always use OnPush
as your change detection strategy in your components for the best performance.
All you have to do to get this local change detection is to declare the change detection strategy of a component as OnPush
and use signals as state sources in the component template.
New control flow in templates
Previously, Angular used to only allow control flows using structural directives eg. *ngIf and *ngFor. The problem with these existing control flows was that they had to be attached to a DOM element and didn’t stand out from the HTML code.
The new approach looks more like how we know it from server rendering frameworks like ASP.NET with the @
syntax:
@Component({ selector: 'app-todo-list', template: `@if (this.isLoading() === false) { <div class="todo-list-wrapper"> <div class="mx-auto col-10"> <h5>{{ 'todo-list' | translate }}</h5> <hr /> <app-cards-list [tableRef]="todoListRef" [cardRef]="todoItemCardRef" [data]="todoList()"></app-cards-list> <hr /> <div> {{ 'todo-list-section.todos-duedate-today' | translate }}: {{ todoList() | duedateTodayCount }} </div> <hr /> <app-add-todo-reactive-forms [currentTodo]="selectedTodo$ | async" [isSavingTodo]="isSavingTodo$ | async" (saveTodo)="onSaveTodo($event)"></app-add-todo-reactive-forms> </div> </div> } @else { <app-spinner [message]="'Getting todo items'"></app-spinner> } <ng-template #todoItemCardRef let-todo="data"> <app-todo-item-card [todoItem]="todo" (todoDelete)="deleteTodo($event)" (todoEdit)="selectTodoForEdit($event)" (todoCompleteToggled)="todoCompleteToggled($event)"></app-todo-item-card> </ng-template> <ng-template #todoListRef let-todos="data"> <ul class="list-group mb-3"> @for (todo of todos; track todo.id) { <app-crud-item [todoItem]="todo" (todoDelete)="deleteTodo($event)" (todoEdit)="selectTodoForEdit($event)" (todoCompleteToggled)="todoCompleteToggled($event)"></app-crud-item> } </ul> </ng-template>`, standalone: true, imports: [SharedModule, DuedateTodayCountPipe], }) export class TodoListComponent { public selectedTodo$ = this.todoListFacadeService.selectedTodo$; public todoList = this.todoListFacadeService.todoList; public isLoading = this.todoListFacadeService.isLoading; public isSavingTodo$ = this.todoListFacadeService.isSavingTodo$;
We see how it stands out more clearly from the existing code.
If else
Before, doing an if else in Angular was rather… Clumsy. You had to reference another template reference in the else which made the code rather incoherent.
With the new control flow, it is much more intuitive to how an if/else control flow would like in any other programming language:
@if (this.isLoading() === false) { <!-- Some content --> } @else { <app-spinner [message]="'Getting todo items'"></app-spinner> }
For loop
The for loop is also more intuitive and aligned with how you would do in other frameworks:
@for (todo of todos; track todo.id) { <app-crud-item [todoItem]="todo" (todoDelete)="deleteTodo($event)" (todoEdit)="selectTodoForEdit($event)" (todoCompleteToggled)="todoCompleteToggled($event)"></app-crud-item> }
Switch case
Switch case especially shows how the new control flow is beneficial as it makes the HTML code stand out even with this control flow:
@switch (getQuestionRendering(question).toString()) { @case ('TEXTBOX') { <input [formControlName]="question.externalQuestionId" [id]="question.externalQuestionId" /> } @case ('NUMBER') { <input appNumberInput [formControlName]="question.externalQuestionId" [id]="question.externalQuestionId" /> }
Migrate application to new control flow
You can automatically migrate your application to the new control flow using this schematic:
ng g @angular/core:control-flow
Delayed loading
A common technique to make your websites load faster is to defer everything “below the fold” (outside of the viewport). Before, you had to use many libraries for this or do it yourself with a directive and the intersection observer API.
Since Angular 17 Angular now has this built into the new control flow with @defer
. We can then simply defer the load until it is visible in the viewport like:
import { Component } from '@angular/core'; @Component({ selector: 'app-home', template: ` <app-navbar></app-navbar> <app-above-page-content></app-above-page-content> @defer (on viewport) { <app-page-content> </app-page-content> } @placeholder { <app-skeleton></app-skeleton> } @error { <app-error message="loading failed"></app-error> } ` }) export class HomeComponent { }
Using @defer
delays the loading of the enclosed content until a specific event happens.
Defer API
The defer control flow follows this syntax:
@defer (on/when *trigger*; prefetch on *trigger*)
The @defer triggers encompass two distinct types:
- Declarative (on): Utilizes one of the available behaviors, as detailed below.
- Imperative (when): Relies on custom logic, such as a component property, method, Signal, RxJs stream, etc., returning true or false.
Declarative “on” triggers: Explore the declarative @defer (on <trigger>) {} triggers, ordered from the most eager to the laziest or custom options:
- immediate: Triggers component lazy loading immediately during the parent component template execution.
- idle (default): Angular lazily loads the component on the first available requestIdleCallback (browser API), enabling background and low-priority tasks on the main event loop.
- timer(delay): Loads the component after a specified delay.
- viewport (target): Activates when the @placeholder (explained below) or an optional target is in the viewport, detected using the browser’s IntersectionObserver API.
- hover (target?): Initiates when the @placeholder or optional target is hovered by the user, with Angular considering mouseenter and focusin DOM events.
- interaction (target?): Triggers when the @placeholder or optional target is interacted with by the user, with Angular recognizing click and keydown DOM events for this purpose.
Build performance with esbuild
Previously Angular relied on Webpack for bundling but there are faster alternatives. Angular now supports esbuild which is a much faster bundler:
You can simply switch to esbuild by changing your Angular build executor:
"builder" : "@angular-devkit/build-angular:browser-esbuild"
SSR improvements
It’s simple to set up an Angular app with SSR. simply run ng new new-app –ssr and you have a new Angular app with SSR.
You can also add SSR to an existing app by simply run:
ng add @angular/ssr
Conclusion
There are many interesting new features in Angular 17. Not only are Signals more mature and come with more benefits such as local change detection but also the control flow of the framework is more aligned with what you see from other frameworks. The defer
feature is something that you before had to use third-party libraries to accomplish but now it’s all built in.
Do you want to become an Angular architect? Check out Angular Architect Accelerator.