Based on the informations provided here "Modern Angular





Manfred Steyer




© 2022 - 2024 Manfred Steyer





Table of Contents


Intro Structure

Help to Improve this Book!

Spread the Word!

Trainings and Consulting





Standalone Components: Mental Model & Compatibility Why Did we Even Get NgModules in the First Place?

Getting Started With Standalone Components

The Mental Model

Pipes, Directives, and Services

Bootstrapping Standalone Components

Compatibility With Existing Code

Side Note: The CommonModule

Conclusion





Architecture with Standalone Components Grouping Building Blocks

Importing Whole Barrels

Barrels with Pretty Names: Path Mappings

Workspace Libraries and Nx

Module Boundaries with Sheriff

Conclusion





Standalone APIs for Routing and Lazy Loading Providing the Routing Configuration

Using Router Directives

Lazy Loading with Standalone Components

Environment Injectors: Services for Specific Routes

Setting up NGRX and Feature Slices

Setting up Your Environment: ENVIRONMENT_INITIALIZER

Component Input Bindings

Conclusion





Angular Elements with Standalone Components Providing a Standalone Component

Installing Angular Elements

Bootstrapping with Angular Elements

Side Note: Bootstrapping Multiple Components

Calling an Angular Element

Calling a Web Component in an Angular Component

Bonus: Compiling Self-contained Bundle

Conclusion





The Refurbished HttpClient - Standalone APIs and Functional Interceptors Standalone APIs for HttpClient

Functional Interceptors

Interceptors and Lazy Loading

Pitfall with withRequestsMadeViaParent

Legacy Interceptors and Other Features

Further Features

Conclusion





Testing Angular Standalone Components Test Setup

The HttpClient Mock

Shallow Testing

Mock Router and Store

Conclusion





Patterns for Custom Standalone APIs in Angular Case Study for Patterns

The Golden Rule

Pattern: Provider Factory

Pattern: Feature

Pattern: Configuration Provider Factory

Pattern: NgModule Bridge

Pattern: Service Chain

Pattern: Functional Service

Conclusion





How to prepare for Standalone Components? Option 1: Ostrich Strategy

Option 2: Just Throw Away Angular Modules

Option 2a: Automatic Migration to Standalone

Option 3: Replace Angular Modules with Barrels

Option 4: Nx Workspace with Libraries and Linting Rules

Option 4a: Folder-based Module Boundaries with Sheriff

Conclusion





Automatic Migration to Standalone Components in 3 Steps A First Look at the Application to Migrate

Step 1

Step 2

Step 3

Bonus: Moving to Standalone APIs

Conclusion





Signals in Angular: The Future of Change Detection Change Detection Today: Zone.js

Change Detection Tomorrow: Signals

Using Signals

Updating Signals

Signals Need to be Immutable

Calculated Values, Side Effects, and Assertions

Effects Need an Injection Context

Writing Signals in Effects

Signals and Change Detection

RxJS Interop

NGRX and Other Stores?

Conclusion





Component Communication with Signals: Inputs, Two-Way Bindings, and Content/ View Queries Input Signals

Two-Way Data Binding with Model Signals

Content Queries with Signals

Output API

View Queries with Signals

Queries and ViewContainerRef

Feature Parity between Content and View Queries

Conclusion





Successful with Signals in Angular - 3 Effective Rules for Your Architecture Initial Example With Some Room for Improvement

Rule 1: Derive State Synchronously Wherever Possible

Rule 2: Avoid Effects for Propagating State

Rule 3: Stores Simplify Reactive Data Flow

Conclusion





Built-in Control Flow and Deferrable Views New Syntax for Control Flow in Templates

Automatic Migration to Build-in Control Flow

Delayed Loading

Conclusion





esbuild and the new Application Builder Build Performance with esbuild

SSR Without Effort with the new Application Builder

More than SSR: Non-destructive Hydration

More Details on Hydration in Angular

Conclusion





About the Author

Trainings and Consulting





Intro


At the beginning of 2023, Sarah Drasner, who as Director of Engineering at Google also heads the Angular team, coined the term Angular Renaissance. This term means a renewal of the framework that has supported us in the development of modern JavaScript solutions for seven years now.

This renewal is incremental and backwards compatible and takes current trends from the world of front-end frameworks into account. This is primarily about developer experience and performance. Standalone Components and Signals are two well-known features that have already emerged as part of this movement.

In this book, I discuss the innovations that come with the Angular Renaissance using several examples.





Structure


This book is subdivided into 14 chapters grouped to four parts discussing different aspects of modern Angular.





Part 1: Standalone Components


The first part discusses Standalone Components, how they play together with traditional NgModule-based code, and what they mean for your architecture.

Chapters in part 1:

Standalone Components: Mental Model & Compatibility

Architecture with Standalone Components





Part 2: Improved APIs


This part goes in depth with the new Standalone APIs – renewed Angular APIs for routing, lazy loading, http access, web components, and testing.

Chapters in part 2:

Standalone APIs for Routing and Lazy Loading

Angular Elements with Standalone Components

The Refurbished HttpClient - Standalone APIs and Functional Interceptors

Testing Angular Standalone Components

Patterns for Custom Standalone APIs in Angular





Part 3: Preparing for and Migrating to Standalone


In this part you learn how to migrate your existing code to Standalone.

Chapters in part 3:

How to prepare for Standalone Components?

Automatic Migration to Standalone Components in 3 Steps





Part 4: Signals


Signals are the future of change detection in Angular. The fourth part shows how to use them in your applications.

Chapters in part 4:

Signals in Angular: The Future of Change Detection

Component Communication with Signals: Inputs, Two-Way Bindings, and Content/ View Queries

Successful with Signals in Angular - 3 Effective Rules for Your Architecture





Part 5: Control Flow & Performance


The final part explains what’s behind the new control flow syntax, how to improve the performance with deferable views, SSR, and hydration, and how to speed up the build with the new esbuild-based ApplicationBuilder.

Chapters in part 5:

Built-in Control Flow and Deferrable Views

esbuild and the new Application Builder





Help to Improve this Book!


Please let us know if you have any suggestions or find any mistakes. Just open an issue at or send a pull request to the book’s GitHub repository.





Spread the Word!


If you like this book, tell your contacts via Twitter/X, Facebook, and/or LinkedIn about it.

Also, feel free to send the download link to colleagues and friends via email or your company-internal chat platform like Slack or Teams.





Trainings and Consulting


If you and your team need support or trainings regarding Angular, we are happy to help with workshops and consulting (on-site or remote). In addition to several other kinds of workshop, we provide the following ones:

Advanced Angular: Enterprise Solutions and Architecture

Angular Essentials: Building Blocks and Concepts

Modern Angular Workshop

Angular Micro Frontends Workshop

Angular Testing Workshop (Cypress, Jest, etc.)

Angular Performance Workshop

Angular Design Systems Workshop (Figma, Storybook, etc.)

Angular: Reactive Architectures (RxJS and NGRX)

Angular Review Workshop

Angular Upgrade Workshop



Please find the full list of our offers here.

Modern Angular Workshop

Modern Angular (English) | Modern Angular (German)

We provide our workshops and consulting in various forms: remote or on-site; public or as dedicated company workshops; in English or in German.

If you have any questions, reach out to us at <PRESIDIO_ANONYMIZED_EMAIL_ADDRESS>.





Standalone Components: Mental Model & Compatibility


Standalone Components is one of the most exciting new Angular features since quite a time. They allow for working without NgModules and hence are the key for more lightweight and straightforward Angular solutions.

In this book, I’m going to demonstrate how to leverage this innovation. For this, I’m using an example application completely written with Standalone Components.

The source code for this can be found in the form of a traditional Angular CLI workspace and as an Nx workspace that uses libraries as a replacement for NgModules.





Why Did we Even Get NgModules in the First Place?


The main reason for initially introducing NgModules was pragmatic: We needed a way to group building blocks that are used together. Not only to increase the convenience for developers, but also for the Angular Compiler whose development lagged a little behind. In the latter case, we are talking about the compilation context. From this context, the compiler learns where the program code is allowed to call which components:

NgModules provide the Compilation Context

However, the community was never really happy with this decision. Having another modular system besides that of EcmaScript didn’t feel right. In addition, it raised the entry barrier for new Angular developers. That is why the Angular team designed the new Ivy compiler so that the compiled application works without modules at runtime. Each component compiled with Ivy has its own compilation context. Even if that sounds grandiose, this context is just represented by two arrays that refer to adjacent components, directives, and pipes.

Since the old compiler and the associated execution environment have now been permanently removed from Angular as of Angular 13, it was time to anchor this option in Angular’s public API. For some time there has been a design document and an associated RFC [RFC]. Both describe a world where Angular modules are optional. The word optional is important here: Existing code that relies on modules is still supported.





Getting Started With Standalone Components


In general, implementing a Standalone Component is easy. Just set the standalone flag in the Component decorator to true and import everything you want to use:

1 @Component({ 2 standalone: true, 3 selector: 'app-root', 4 imports: [ 5 RouterOutlet, 6 NavbarComponent, 7 SidebarComponent, 8 ], 9 templateUrl: './app.component.html', 10 styleUrls: ['./app.component.css'] 11 }) 12 export class AppComponent { 13 [...] 14 }



The imports define the compilation context: all the other building blocks the Standalone Components is allowed to use. For instance, you use it to import further Standalone Component, but also existing NgModules.

The exhaustive listing of all these building blocks makes the component self-sufficient and thus increases its reusability in principle. It also forces us to think about the component’s dependencies. Unfortunately, this task turns out to be extremely monotonous and time consuming.

Therefore, there are considerations to implement a kind of auto-import in the Angular Language Service used by the IDEs. Analogous to the auto-import for TypeScript modules, the IDE of choice could also suggest placing the corresponding entry in the imports array the first time a component, pipe or directive is used in the template.





The Mental Model


The underlying mental model helps to better understand Standalone Components. In general, you can imagine a Standalone Component as a component with its very own NgModule:

Mental Model

This is similar to Lars Nielsen’s SCAM pattern. However, while SCAM uses an explicit module, here we only talk about a thought one.

While this mental model is useful for understanding Angular’s behavior, it’s also important to see that Angular doesn’t implement Standalone Components that way underneath the covers.





Pipes, Directives, and Services


Analogous to standalone components, there are also standalone pipes and standalone directives. For this purpose, the pipe and directive decorators also get a standalone property. This is what a standalone pipe will look alike:

1 @Pipe ({ 2 standalone: true, 3 name: 'city', 4 pure: true 5 }) 6 export class CityPipe implements PipeTransform { 7 8 transform (value: string, format: string): string {[…]} 9 10 }



And here is an example for a standalone directive:

1 @Directive ({ 2 standalone: true, 3 selector: 'input [appCity]', 4 providers: […] 5 }) 6 export class CityValidator implements Validator { 7 8 [...] 9 10 }



Thanks to tree-shakable providers, on the other hand, services have worked without modules for quite a time. For this purpose the property providedIn has to be used:

1 @Injectable ({ 2 providedIn: 'root' 3 }) 4 export class FlightService {[…]}



The Angular team recommends, to use providedIn: 'root' whenever possible. It might come as a surprise, but providedIn: 'root' also works with lazy loading: If you only use a service in lazy parts of your application, it is loaded alongside them.





Bootstrapping Standalone Components


Until now, modules were also required for bootstrapping, especially since Angular expected a module with a bootstrap component. Thus, this so called AppModule or “root module” defined the main component alongside its compilation context.

With Standalone Components, it will be possible to bootstrap a single component. For this, Angular provides a method bootstrapApplication which can be used in main.ts:

1 // main.ts 2 3 import { bootstrapApplication } from '@angular/platform-browser'; 4 import { provideAnimations } from '@angular/platform-browser/animations'; 5 import { AppComponent } from './app/app.component'; 6 import { APP_ROUTES } from './app/app.routes'; 7 import { provideRouter } from '@angular/router'; 8 import { importProvidersFrom } from '@angular/core'; 9 10 [...] 11 12 bootstrapApplication(AppComponent, { 13 providers: [ 14 importProvidersFrom(HttpClientModule), 15 provideRouter(APP_ROUTES), 16 provideAnimations(), 17 importProvidersFrom(TicketsModule), 18 importProvidersFrom(LayoutModule), 19 ] 20 });



The first argument passed to bootstrapApplication is the main component. Here, it’s our AppComponent. Via the second argument, we pass application-wide service providers. These are the providers, you would register with the AppModule when going with NgModules.

The provided helper function importProvidersFrom allows bridging the gap to existing NgModules. Please also note, that importProvidersFrom works with both NgModules but also ModuleWithProviders as returned by methods like forRoot and forChild.

While this allows to immediately leverage existing NgModule-based APIs, we will see more and more functions that replace the usage of importProvidersFrom in the future. For instance, to register the router with a given configuration, the function provideRouter is used. Similarly, provideAnimations setup up the Animation module.





Compatibility With Existing Code


As discussed above, according to the mental model, a Standalone Component is just a component with its very own NgModule. This is also the key for the compatibility with existing code still using NgModules.

On the one side, we can import whole NgModules into a Standalone Component:

1 import { Component, OnInit } from '@angular/core'; 2 import { TicketsModule } from '../tickets/tickets.module'; 3 4 @Component({ 5 selector: 'app-next-flight', 6 standalone: true, 7 imports: [ 8 // Existing NgModule imported 9 // in this standalone component 10 TicketsModule 11 ], 12 [...] 13 }) 14 export class NextFlightComponent implements OnInit { 15 [...] 16 }



But on the other side, we can also import a Standalone Component (Directive, Pipe) into an existing NgModule:

1 @NgModule({ 2 imports: [ 3 CommonModule, 4 5 // Imported Standalone Component: 6 FlightCardComponent, 7 [...] 8 ], 9 declarations: [ 10 MyTicketsComponent 11 ], 12 [...] 13 }) 14 export class TicketsModule { }



Interestingly, standalone components are imported like modules and not declared like classic components. This may be confusing at first glance, but it totally fits the mental model that views a standalone component a component with its very own NgModule.

Also, declaring a traditional component defines a strong whole-part relationship: A traditional component can only be declared by one module and then, it belongs to this module. However, a standalone component doesn’t belong to any NgModule but it can be reused in several places. Hence, using imports here really makes sense.





Side Note: The CommonModule


Doubtless, one of the most known NgModules in Angular was the CommonModule. It contains built-in directives like *ngIf or *ngFor and built-in pipes like async or json. While you can still import the whole CommonModule, meanwhile it’s also possible to just import the needed directives and pipes:

1 import { 2 AsyncPipe, 3 JsonPipe, 4 NgForOf, 5 NgIf 6 } from "@angular/common"; 7 8 [...] 9 10 @Component({ 11 standalone: true, 12 imports: [ 13 // CommonModule, 14 NgIf, 15 NgForOf, 16 AsyncPipe, 17 JsonPipe, 18 19 FormsModule, 20 FlightCardComponent, 21 CityValidator, 22 ], 23 selector: 'flight-search', 24 templateUrl: './flight-search.component.html' 25 }) 26 export class FlightSearchComponent implements OnInit { 27 [...] 28 }



This is possible, because the Angular team made Standalone Directives and Standalone Pipes out of the building blocks provided by the CommonModule. Importing these building blocks in a fine grained way will be especially interesting once IDEs provide auto-imports for standalone components. In this case, the first usage of an building block like *ngIf will make the IDE to add it to the imports array.

As we will see in a further part of this book, meanwhile also the RouterModule comes with Standalone building-blocks. Hence, we can directly import RouterOutlet instead of going with the whole RouterModule. When writing this, this was not yet possible for other modules like the FormsModule or the HttpClientModule.





Conclusion


So far we’ve seen how to use Standalone Components to make our Angular applications more lightweight. We’ve also seen that the underlying mental model guarantees compatibility with existing code.

However, now the question arises how this all will influence our application structure and architecture. The next chapter will shed some light on this.





Architecture with Standalone Components


In last chapter, I’ve shown how Standalone Components will make our Angular applications more lightweight in the future. In this part, I’m discussing options for improving your architecture with them.

The source code for this can be found in the form of a traditional Angular CLI workspace and as an Nx workspace that uses libraries as a replacement for NgModules.





Grouping Building Blocks


Unfortunately, the examples shown so far cannot keep up with one aspect of NgModules. Namely the possibility of grouping building blocks that are usually used together.

Obviously, the easiest approach for grouping stuff that goes together is using folders. However, you might go one step further by leveraging barrels: A barrel is an EcmaScript file that exports related elements.

These files are often called public-api.ts or index.ts. The example project used contains such an index.ts to group two navigation components from the shell folder:

Grouping two Standalone Components with a barrel

The barrel itself re-exports the two components:

1 export { NavbarComponent } from './navbar/navbar.component'; 2 export { SidebarComponent } from './sidebar/sidebar.component';



The best of this is, you get real modularization: Everything the barrel experts can be used by other parts of your application. Everything else is your secret. You can modify these secrets as you want, as long as the public API defined by your barrel stays backwards compatible.

In order to use the barrel, just point to it with an import:

1 import { 2 NavbarComponent, 3 SidebarComponent 4 } from './shell/index'; 5 6 @Component({ 7 standalone: true, 8 selector: 'app-root', 9 imports: [ 10 RouterOutlet, 11 12 NavbarComponent, 13 SidebarComponent, 14 ], 15 templateUrl: './app.component.html', 16 styleUrls: ['./app.component.css'] 17 }) 18 export class AppComponent { 19 [...] 20 }



If you call your barrel index.ts, you can even omit the file name, as index is the default name when configuring the TypeScript compiler to use Node.js-based conventions. Something that is the case in the world of Angular and the CLI:

1 import { 2 NavbarComponent, 3 SidebarComponent 4 } from './shell'; 5 6 @Component({ 7 standalone: true, 8 selector: 'app-root', 9 imports: [ 10 RouterOutlet, 11 12 NavbarComponent, 13 SidebarComponent, 14 ], 15 templateUrl: './app.component.html', 16 styleUrls: ['./app.component.css'] 17 }) 18 export class AppComponent { 19 [...] 20 }





Importing Whole Barrels


In the last section, the NavbarComponent and the SidebarComponent were part of the shell’s public API. Nevertheless. Angular doesn’t provide a way to import everything a barrel provides at once.

In most of the cases, this is the totally fine. Auto-imports provided by your IDE will add the needed entries anyway. Also, being explicit about what you need helps enables tree-shaking.

However, in some edge-cases where you know that some building blocks always go together, e. g. because there is a strong mutual dependency, putting them into an array can help to make our lives easier. For instance, think about all the directives provided by the FormsModule. Normally, we don’t even know their exact names nor which of them play together.

The following example demonstrates this idea:

1 import { NavbarComponent } from './navbar/navbar.component'; 2 import { SidebarComponent } from './sidebar/sidebar.component'; 3 4 export { NavbarComponent } from './navbar/navbar.component'; 5 export { SidebarComponent } from './sidebar/sidebar.component'; 6 7 export const SHELL = [ 8 NavbarComponent, 9 SidebarComponent 10 ];



Interestingly, such arrays remind us to the exports section of NgModules. Please note that your array needs to be a constant. This is needed because the Angular Compiler uses it already at compile time.

Such arrays can be directly put into the imports array. No need for spreading them:

1 import { SHELL } from './shell'; 2 3 [...] 4 5 @Component({ 6 standalone: true, 7 selector: 'app-root', 8 imports: [ 9 RouterOutlet, 10 11 // NavbarComponent, 12 // SidebarComponent, 13 SHELL 14 ], 15 templateUrl: './app.component.html', 16 styleUrls: ['./app.component.css'] 17 }) 18 export class AppComponent { 19 [...] 20 }



One more time I want to stress out that this array-based style should only be used with caution. While it allows to group things that always go together it also makes your code less tree-shakable.





Barrels with Pretty Names: Path Mappings


Just using import statements that directly point to other parts of your application often leads to long relative and confusing paths:

1 import { SHELL } from '../../../../shell'; 2 3 @Component ({ 4 standalone: true, 5 selector: 'app-my-cmp', 6 imports: [ 7 SHELL, 8 [...] 9 ] 10 }) 11 export class MyComponent { 12 }



To bypass this, you can define path mappings for your barrels you import from in your TypeScript configuration (tsconfig.json in the project’s root):

1 "paths": { 2 "@demo/shell": ["src/app/shell/index.ts"], 3 [...] 4 }



This allows direct access to the barrel using a well-defined name without having to worry about - sometimes excessive - relative paths:

1 // Import via mapped path: 2 import { SHELL } from '@demo/shell'; 3 4 @Component ({ 5 standalone: true, 6 selector: 'app-root', 7 imports: [ 8 SHELL, 9 [...] 10 ] 11 }) 12 export class MyComponent { 13 }





Workspace Libraries and Nx


These path mappings can of course be created manually. However, it is a little easier with the CLI extension Nx which automatically generates such path mappings for each library created within a workspace. Libraries seem to be the better solution anyway, especially since they subdivide it more and Nx prevents bypassing the barrel of a library.

This means that every library consists of a public – actually published – and a private part. The library’s public and private APIs are also mentioned here. Everything the library exports through its barrel is public. The rest is private and therefore a “secret” of the library that other parts of the application cannot access.

It is precisely these types of “secrets” that are a simple but effective key to stable architectures, especially since everything that is not published can easily be changed afterwards. The public API, on the other hand, should only be changed with care, especially since a breaking change can cause problems in other areas of the project.

An Nx project (workspace) that represents the individual sub-areas of the Angular solution as libraries could use the following structure:

Structure of an Nx Solution

Each library receives a barrel that reflects the public API. The prefixes in the library names reflect a categorization recommended by the Nx team. For example, feature libraries contain smart components that know the use cases, while UI libraries contain reusable dumb components. The domain library comes with the client-side view of our domain model and the services operating on it, and utility libraries contain general auxiliary functions.

On the basis of such categories, Nx allows the definition of linting rules that prevent undesired access between libraries. For example, you could specify that a domain library should only have access to utility libraries and not to UI libraries:

Nx prevents unwanted access between libraries via linting

In addition, Nx allows the dependencies between libraries to be visualized:

Nx visualizes the dependencies between libraries

If you want to see all of this in action, feel free to have a look at the Nx version of the example used here. Your find the Source Code at GitHub.

Besides enforcing module boundaries, Nx also comes with several additional important features: It allows for an incremental CI/CD that only rebuilds and retests parts of the monorepo that have been actually affected by a code change. Also, together with the Nx Cloud it allows to automatically parallelize the whole CI/CD process. Also, it comes with integrations into several useful tools like Storybook, Cypress, or Playwright.





Module Boundaries with Sheriff


Similar to Nx, the open source project Sheriff also allows to enforce module boundaries. However, instead of using libraries for defining modules, it just goes with folders. This makes the application structure more lightweight.

Technically, Sheriff is used as via an eslint plugin. It works with traditional Angular CLI projects but also with Nx. We often combine it with Nx to get the best of both worlds: Incremental CI/CD provided by Nx and lightweight folder-based Module boundaries provided by Sheriff.





Conclusion


Standalone Components make the future of Angular applications more lightweight. We don’t need NgModules anymore. Instead, we just use EcmaScript modules. This makes Angular solutions more straightforward and lowers the entry barrier into the world of the framework. Thanks to the mental model, which regards standalone components as a combination of a component and a NgModule, this new form of development remains compatible with existing code.

For the grouping of related building blocks, simple barrels are ideal for small solutions. For larger projects, the transition to monorepos as offered by the CLI extension Nx seems to be the next logical step. Libraries subdivide the overall solution here and offer public APIs based on barrels. In addition, dependencies between libraries can be visualized and avoided using linting.





Standalone APIs for Routing and Lazy Loading


Since its first days, the Angular Router has always been quite coupled to NgModules. Hence, one question that comes up when moving to Standalone Components is: How will routing and lazy loading work without NgModules? This chapter provides answers and also shows, why the router will become more important for Dependency Injection.

The source code for the examples used here can be found in the form of a traditional Angular CLI workspace and as an Nx workspace that uses libraries as a replacement for NgModules.





Providing the Routing Configuration


When bootstrapping a standalone component, we can provide services for the root scope. These are services you used to provide in your AppModule. Meanwhile, the Router provides a function provideRouter that returns all providers we need to register here:

1 // main.ts 2 3 import { importProvidersFrom } from '@angular/core'; 4 import { bootstrapApplication } from '@angular/platform-browser'; 5 import { 6 PreloadAllModules, 7 provideRouter, 8 withDebugTracing, 9 withPreloading, 10 withRouterConfig 11 } 12 from '@angular/router'; 13 14 import { APP_ROUTES } from './app/app.routes'; 15 [...] 16 17 bootstrapApplication(AppComponent, { 18 providers: [ 19 importProvidersFrom(HttpClientModule), 20 provideRouter(APP_ROUTES, 21 withPreloading(PreloadAllModules), 22 withDebugTracing(), 23 ), 24 25 [...] 26 27 importProvidersFrom(TicketsModule), 28 provideAnimations(), 29 importProvidersFrom(LayoutModule), 30 ] 31 });



The function provideRouter not only takes the root routes but also the implementation of additional router features. These features are passed with functions having the naming pattern withXYZ, e. g. withPreloading or withDebugTracing. As functions can easily be tree-shaken, this design decisions makes the whole router more tree-shakable.

With the discussed functions, the Angular team also introduces a naming pattern, library authors should follow. Hence, when adding a new library, we just need to look out for an provideXYZ and for some optional withXYZ functions.



As currently not every library comes with a provideXYZ function yet, Angular comes with the bridging function importProvidersFrom. It allows to get hold of all the providers defined in existing NgModules and hence is the key for using them with Standalone Components.

I’m quite sure, the usage of importProvidersFrom will peak off over time, as more and more libraries will provide functions for directly configuring their providers. For instance, NGRX recently introduced a provideStore and a provideEffects function.





Using Router Directives


After setting up the routes, we also need to define a placeholder where the Router displays the activated component and links for switching between them. To get the directives needed for this, you might directly import the RouterModule into your Standalone Component. However, a better alternative is to just import the directives you need:

1 @Component({ 2 standalone: true, 3 selector: 'app-root', 4 imports: [ 5 // Just import the RouterModule: 6 // RouterModule, 7 8 // Better: Just import what you need: 9 RouterOutlet, 10 RouterLinkWithHref, // Angular 14 11 // RouterLink // Angular 15+ 12 13 NavbarComponent, 14 SidebarComponent, 15 ], 16 templateUrl: './app.component.html', 17 styleUrls: ['./app.component.css'] 18 }) 19 export class AppComponent { 20 [...] 21 }



Just importing the actually needed directives is possible, because the router exposes them as Standalone Directives. Please note that in Angular 14, RouterLinkWithHref is needed if you use routerLink with an a-tag; in all other cases you should import RouterLink instead. As this is a bit confusing, the Angular Team refactored this for Angular 15: Beginning with this version, RouterLink is used in all cases.

In most cases, this is nothing we need to worry about when IDEs start providing auto-imports for Standalone Components.





Lazy Loading with Standalone Components


In the past, a lazy route pointed to an NgModule with child routes. As there are no NgModules anymore, loadChildren can now directly point to a lazy routing configuration:

1 // app.routes.ts 2 3 import { Routes } from '@angular/router'; 4 import { HomeComponent } from './home/home.component'; 5 6 export const APP_ROUTES: Routes = [ 7 { 8 path: '', 9 pathMatch: 'full', 10 redirectTo: 'home' 11 }, 12 { 13 path: 'home', 14 component: HomeComponent 15 }, 16 17 // Option 1: Lazy Loading another Routing Config 18 { 19 path: 'flight-booking', 20 loadChildren: () => 21 import('./booking/flight-booking.routes') 22 .then(m => m.FLIGHT_BOOKING_ROUTES) 23 }, 24 25 // Option 2: Directly Lazy Loading a Standalone Component 26 { 27 path: 'next-flight', 28 loadComponent: () => 29 import('./next-flight/next-flight.component') 30 .then(m => m.NextFlightComponent) 31 }, 32 [...] 33 ];



This removes the indirection via an NgModule and makes our code more explicit. As an alternative, a lazy route can also directly point to a Standalone Component. For this, the above shown loadComponent property is used.

I expect that most teams will favor the first option, because normally, an application needs to lazy loading several routes that go together.





Environment Injectors: Services for Specific Routes


With NgModules, each lazy module introduced a new injector and hence a new injection scope. This scope was used for providing services only needed by the respective lazy chunk.

To cover such use cases, the Router now allows for introducing providers for each route. These services can be used by the route in question and their child routes:

1 // booking/flight-booking.routes.ts 2 3 export const FLIGHT_BOOKING_ROUTES: Routes = [{ 4 path: '', 5 component: FlightBookingComponent, 6 providers: [ 7 provideBookingDomain(config) 8 ], 9 children: [ 10 { 11 path: '', 12 pathMatch: 'full', 13 redirectTo: 'flight-search' 14 }, 15 { 16 path: 'flight-search', 17 component: FlightSearchComponent 18 }, 19 { 20 path: 'passenger-search', 21 component: PassengerSearchComponent 22 }, 23 { 24 path: 'flight-edit/:id', 25 component: FlightEditComponent 26 } 27 ] 28 }];



As shown here, we can provide services for several routes by grouping them as child routes. In these cases, a component-less parent route with an empty path (path: '') is used. This pattern is already used for years to assign Guards to a group of routes.

Technically, using adding a providers array to a router configuration introduces a new injector at the level of the route. Such an injector is called Environment Injector and replaces the concept of the former (Ng)Module Injectors. The root injector and the platform injector are further Environment Injectors.

Interestingly, this also decouples lazy loading from introducing further injection scopes. Previously, each lazy NgModule introduced a new injection scope, while non-lazy NgModules never did. Now, lazy loading itself doesn’t influence the scopes. Instead, now, you define new scopes by adding a providers array to your routes, regardless if the route is lazy or not.

The Angular team recommends to use this providers array with caution and to favor providedIn: 'root' instead. As already mentioned in a previous chapter, also providedIn: 'root' allows for lazy loading. If you just use a services provided with providedIn: 'root' in lazy parts of your application, they will only be loaded together with them.

However, there is one situation where providedIn: 'root' does not work and hence the providers array shown is needed, namely if you need to pass a configuration to a library. I’ve already indicated this in the above example by passing a config object to my custom provideBookingDomain. The next section provides a more elaborated example for this using NGRX.





Setting up NGRX and Feature Slices


To illustrate how to use libraries adopted for Standalone Components with lazy loading, let’s see how to setup NGRX. Let’s start with providing the needed global services:

1 import { bootstrapApplication } from '@angular/platform-browser'; 2 3 import { provideStore } from '@ngrx/store'; 4 import { provideEffects } from '@ngrx/effects'; 5 import { provideStoreDevtools } from '@ngrx/store-devtools'; 6 7 import { reducer } from './app/+state'; 8 9 [...] 10 11 bootstrapApplication(AppComponent, { 12 providers: [ 13 importProvidersFrom(HttpClientModule), 14 provideRouter(APP_ROUTES, 15 withPreloading(PreloadAllModules), 16 withDebugTracing(), 17 ), 18 19 // Setup NGRX: 20 provideStore(reducer), 21 provideEffects([]), 22 provideStoreDevtools(), 23 24 importProvidersFrom(TicketsModule), 25 provideAnimations(), 26 importProvidersFrom(LayoutModule), 27 ] 28 });



For this, we go with the functions provideStore, provideEffects, and provideStoreDevtools NGRX comes with since version 14.3.

To allow lazy parts of the application to have their own feature slices, we call provideState and provideEffects in the respective routing configuration:

1 import { provideEffects } from "@ngrx/effects"; 2 import { provideState } from "@ngrx/store"; 3 4 export const FLIGHT_BOOKING_ROUTES: Routes = [{ 5 path: '', 6 component: FlightBookingComponent, 7 providers: [ 8 provideState(bookingFeature), 9 provideEffects([BookingEffects]) 10 ], 11 children: [ 12 { 13 path: 'flight-search', 14 component: FlightSearchComponent 15 }, 16 { 17 path: 'passenger-search', 18 component: PassengerSearchComponent 19 }, 20 { 21 path: 'flight-edit/:id', 22 component: FlightEditComponent 23 } 24 ] 25 }];



While provideStore sets up the store at root level, provideState sets up additional feature slices. For this, you can provide a feature or just a branch name with a reducer. Interestingly, the function provideEffects is used at the root level but also at the level of lazy parts. Hence, it provides the initial effects but also effects needed for a given feature slice.





Setting up Your Environment: ENVIRONMENT_INITIALIZER


Some libraries used the constructor of lazy NgModule for their initialization. To further support this approach without NgModules, there is now the concept of an ENVIRONMENT_INITIALIZER:

1 export const FLIGHT_BOOKING_ROUTES: Routes = [{ 2 path: '', 3 component: FlightBookingComponent, 4 providers: [ 5 importProvidersFrom(StoreModule.forFeature(bookingFeature)), 6 importProvidersFrom(EffectsModule.forFeature([BookingEffects])), 7 { 8 provide: ENVIRONMENT_INITIALIZER, 9 multi: true, 10 useValue: () => inject(InitService).init() 11 } 12 ], 13 children: [ 14 [...] 15 ] 16 }



Basically, the ENVIRONMENT_INITIALIZER provides a function executed when the Environment Injector is initialized. The flag multi: true already indicates that you can have several such initializers per scope.





Component Input Bindings


The router has also received a few nice roundings. For example, it can now be instructed to pass routing parameters directly to inputs of the respective component. For example, if a route is called with ;q=Graz, the router assigns the value Graz to the input with the name q:

1 @Input ( ) q = '' ;



Retrieving the parameter values via the ActivatedRoute service is no longer necessary. This behavior applies to parameters in the data object, in the query string, as well as to the matrix parameters that are usual in Angular. In the event of a conflict, this order also applies, e.g. if present, the value is taken from the data object, otherwise the query string is checked and then the matrix parameters. In order not to disrupt existing code, this option must be explicitly activated. For this, the withComponentInputBinding function is used when calling provideRouter:

1 provideRouter( 2 APP_ROUTES, 3 withComponentInputBinding() 4 ),



In addition, the router now has a lastSuccessfulNavigation property that provides information about the current route:

1 router = inject(Router); 2 […] 3 console.log( 4 'lastSuccessfullNavigation', 5 this.router.lastSuccessfulNavigation 6 );





Conclusion


The streamlined router API removes unnecessary indirections for lazy loading: Instead of pointing to a lazy NgModule, a routing configuration now directly points to another lazy routing configuration. Providers we used to register with lazy NgModules, e.g. providers for a feature slice, are directly added to the respective route and can also be used in every child route.





Angular Elements with Standalone Components


Since Angular 14.2, it’s possible to use Standalone Components as Angular Elements. In this chapter, I’m going to show you, how this new feature works.

Source Code





Providing a Standalone Component


The Standalone Component I’m going to use here is a simple Toggle Button called ToggleComponent:

1 import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/\ 2 core'; 3 import { CommonModule } from '@angular/common'; 4 5 @Component({ 6 selector: 'app-toggle', 7 standalone: true, 8 imports: [], 9 template: ` 10 <div class="toggle" [class.active]="active" (click)="toggle()"> 11 <slot>Toggle!</slot> 12 </div> 13 `, 14 styles: [` 15 .toggle { 16 padding:10px; 17 border: solid black 1px; 18 cursor: pointer; 19 display: inline 20 } 21 22 .active { 23 background-color: lightsteelblue; 24 } 25 `], 26 encapsulation: ViewEncapsulation.ShadowDom 27 }) 28 export class ToggleComponent { 29 30 @Input() active = false; 31 @Output() change = new EventEmitter<boolean>(); 32 33 toggle(): void { 34 this.active = !this.active; 35 this.change.emit(this.active); 36 } 37 38 }



By setting encapsulation to ViewEncapsulation.ShadowDom, I’m making the browser to use “real” Shadow DOM instead of Angular’s emulated counterpart. However, this also means that we have to use the Browser’s slot API for content projection instead of Angular’s ng-content.





Installing Angular Elements


While Angular Elements is directly provided by the Angular team, the CLI doesn’t install it. Hence, we need to do this by hand:

1 npm i @angular/elements



In former days, @angular/elements also supported ng add. This support came with a schematic for adding a needed polyfill. However, meanwhile, all browsers supported by Angular can deal with Web Components natively. Hence, there is no need for such a polyfill anymore and so the support for ng add was already removed some versions ago.





Bootstrapping with Angular Elements


Now, let’s bootstrap our application and expose the ToggleComponent as a Web Component (Custom Element) with Angular Elements. For this, we can use the function createApplication added with Angular 14.2:

1 // main.ts 2 3 import { createCustomElement } from '@angular/elements'; 4 import { createApplication } from '@angular/platform-browser'; 5 import { ToggleComponent } from './app/toggle/toggle.component'; 6 7 (async () => { 8 9 const app = await createApplication({ 10 providers: [ 11 /* your global providers here */ 12 ], 13 }); 14 15 const toogleElement = createCustomElement(ToggleComponent, { 16 injector: app.injector, 17 }); 18 19 customElements.define('my-toggle', toogleElement); 20 21 })();



We could pass an array with providers to createApplication. This allows to provide services like the HttpClient via the application’s root scope. In general, this option is needed when we want to configure these providers, e. g. with a forRoot method or a provideXYZ function. In all other cases, it’s preferable to just go with tree-shakable providers (providedIn: 'root').

The result of createApplication is a new ApplicationRef. We can pass its Injector alongside the ToggleComponent to createCustomElement. The result is a custom element that can be registered with the browser using customElements.define.

Please note that the current API does not allow for setting an own zone instance like the noop zone. Instead, the Angular team wants to concentrate on new features for zone-less change detection in the future.





Side Note: Bootstrapping Multiple Components


The API shown also allows to create several custom elements:

1 const element1 = createCustomElement(ThisComponent, { 2 injector: app.injector, 3 }); 4 5 const element2 = createCustomElement(ThatComponent, { 6 injector: app.injector, 7 });



Besides working with custom elements, the ApplicationRef at hand also allows for bootstrapping several components as Angular applications:

1 app.injector.get(NgZone).run(() => { 2 app.bootstrap(ToggleComponent, 'my-a'); 3 app.bootstrap(ToggleComponent, 'my-b'); 4 });



When bootstrapping a component this way, one can overwrite the selector to use. Please note, that one has to call bootstrap within a zone in order to get change detection.

Bootstrapping several components was originally done by placing several components in your AppModule’s bootstrap array. The bootstrapApplication function used for bootstrapping Standalone Components does, however, not allow for this as the goal was to provide a simple API for the most common use case.





Calling an Angular Element


To call our Angular Element, we just need to place a respective tag in our index.html:

1 <h1>Standalone Angular Element Demo</h1> 2 <my-toggle id="myToggle">Click me!</my-toggle>



As a custom element is threaded by the browser as a normal DOM node, we can use traditional DOM calls to set up events and to assign values to properties:

1 <script> 2 const myToggle = document.getElementById('myToggle'); 3 4 myToggle.addEventListener('change', (event) => { 5 console.log('active', event.detail); 6 }); 7 8 setTimeout(() => { 9 myToggle.active = true; 10 }, 3000); 11 </script>





Calling a Web Component in an Angular Component


If we call a web component within an Angular component, we can directly data bind to it using brackets for properties and parenthesis for events. This works regardless whether the web component was created with Angular or not.

To demonstrate this, let’s assume we have the following AppComponent:

1 import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 3 @Component({ 4 selector: 'app-root', 5 standalone: true, 6 schemas: [CUSTOM_ELEMENTS_SCHEMA], 7 template: ` 8 <h2>Root Component</h2> 9 <my-toggle 10 [active]="active" 11 (change)="change($event)"> 12 Hello! 13 </my-toggle> 14 `, 15 }) 16 export class AppComponent { 17 active = false; 18 change(event: Event) { 19 const customEvent = event as CustomEvent<boolean>; 20 console.log('active', customEvent.detail); 21 } 22 }



This Standalone Component calls our my-toggle web component. While the Angular compiler is aware of all possible Angular components, it doesn’t know about web components. Hence, it would throw an error when seeing the my-toggle tag. To avoid this, we need to register the CUSTOM_ELEMENTS_SCHEMA schema.

Before, we did this with all the NgModules we wanted to use together with Web Components. Now, we can directly register this schema with Standalone Components. Technically, this just disables the compiler checks regarding possible tag names. This is binary - the checks are either on or off – and there is no way to directly tell the compiler about the available web components.

To make this component appear on our page, we need to bootstrap it:

1 // main.ts 2 3 [...] 4 // Register web components ... 5 [...] 6 7 app.injector.get(NgZone).run(() => { 8 app.bootstrap(AppComponent); 9 });



Also, we need to add an element for AppComponent to the index.html:

1 <app-root></app-root>





Bonus: Compiling Self-contained Bundle


Now, let’s assume, we only provide a custom element and don’t bootstrap our AppComponent. In order to use this custom element in other applications, we need to compile it into a self contained bundle. While the traditional webpack-based builder emits several bundles, e. g. a main bundle and a runtime bundle, the new esbuild-based ApplicationBuilder (see chapter esbuild and the new Application Builder) just gives us one bundle for our source code and another one for the polyfills.

The resulting bundles look like this:

1 948 favicon.ico 2 703 index.html 3 100 177 main.43BPAPVS.js 4 33 916 polyfills.M7XCYQVG.js 5 0 styles.VFXLKGBH.css



If you use your web component in an other web site, e. g. a CMS-driven one, just reference the main bundle there and add a respective tag. Also, reference the polyfills. However, when using several such bundles, you have to make sure, you only load the polyfills once.





Conclusion


As a by-product of Standalone Components, Angular provides a streamlined way for using Angular Elements: We start with creating an ApplicationRef to get an Injector. Alongside a Standalone Component, we pass this injector to Angular Elements. The result is a Web Component we can register with the browser.





The Refurbished HttpClient - Standalone APIs and Functional Interceptors


Without any doubt, the HttpClient is one of the best-known services included in Angular. For version 15, the Angular team has now adapted it for the new standalone components. On this occasion, the interceptor concept was also revised.

In this chapter, I will describe these innovations.

📂 Source Code





Standalone APIs for HttpClient


Beginning with version 15, the HttpClient can be set up without any reference to the HttpClientModule. Instead, we can use provideHttpClient when bootstrapping our application:

1 import { provideHttpClient, withInterceptors } from "@angular/common/http"; 2 3 [...] 4 5 bootstrapApplication(AppComponent, { 6 	providers: [ 7 		provideHttpClient( 8 			withInterceptors([authInterceptor]), 9 		), 10 	] 11 });



This new function also enables optional features of the HttpClient. Each feature has its own function. For example, the withInterceptors function enables support for Http Interceptors.

The combination of a provideXYZ function and several optional withXYZ functions is not chosen arbitrarily here but corresponds to a pattern that the Angular team generally provides for standalone APIs. Application developers must therefore be on the lookout for functions that start with provide or with when setting up a new library.

Also, this pattern leads to a very pleasant side effect: libraries become more tree-shakable. This is because a static source code analysis makes it very easy to find out whether the application ever calls a function. In the case of methods, this is not so easy due to the possibility of polymorphic use of the underlying objects.





Functional Interceptors


When introducing standalone APIs, the Angular team also took the opportunity and revised the HttpClient a bit. One result of this are the new functional interceptors. They allow interceptors to be expressed as simple functions. A separate service that implements a predefined interface is no longer necessary:

1 import { HttpInterceptorFn } from "@angular/common/http"; 2 import { tap } from "rxjs"; 3 4 export const authInterceptor: HttpInterceptorFn = (req, next) => { 5 6 	console.log('request', req.method, req.url); 7 	console.log('authInterceptor') 8 9 	if (req.url.startsWith('https://demo.angulararchitects.io/api/')) { 10 11 		// Setting a dummy token for demonstration 12 13 		constheaders = req.headers.set('Authorization', 'Bearer Auth-1234567'); 14 15 		req = req.clone({ 16 			headers 17 		}); 18 19 	} 20 21 	return next(req).pipes( 22 		tap(resp => console.log('response', resp)) 23 	); 24 25 }



The interceptor shown adds an example security token to HTTP calls that are directed to specific URLs. Except that the interceptor is now a function of type HttpInterceptorFn, the basic functionality of this concept has not changed. As shown above, functional interceptors can be set up using withInterceptors when calling provideHttpClient .





Interceptors and Lazy Loading


Interceptors in lazy modules have always led to confusion: As soon as a lazy module introduces its own interceptors, those of outer scopes – e.g. the root scope – are no longer triggered.

Even if modules with standalone components and APIs are a thing of the past, the basic problem remains, especially since (lazy) route configurations can now set up their own services:

1 export const FLIGHT_BOOKING_ROUTES: Routes = [{ 2 	paths: '', 3 	component: FlightBookingComponent, 4 	providers: [ 5 		MyService, 6 		provideState(bookingFeature), 7 		provideEffects([BookingEffects]) 8 		provideHttpClient( 9 			withInterceptors([bookingInterceptor]), 10 			withRequestsMadeViaParent(), 11 		), 12 	], 13 }];



These services correspond to those the application previously registered in lazy modules. Technically, Angular introduces its own injector whenever such a providers array is available. This so-called environment injector defines a scope for the current route and its child routes.

The new provideHttpClient function can also be used in this providers array to register interceptors for the respective lazy part of the application. By default, the previously discussed rule applies: If there are interceptors in the current environment injector, Angular ignores the interceptors in outer scopes.

Exactly this behavior can be changed with withRequestsMadeViaParent: This method causes Angular to also trigger interceptors in outer scopes.





Pitfall with withRequestsMadeViaParent


The discussed withRequestsMadeViaParent function comes with a non-obvious pitfall: a root-scope service is unaware of inner scope and the interceptors registered there. It always accesses the HttpClient in the root scope and therefore only the interceptors set up there are executed:

Interceptors in multiple scopes

To solve this problem, the application could also register the outer service in the providers array of the route configuration and thus in the inner scope.

In general, however, it seems to be very difficult to keep track of such constellations. Therefore, it might make sense to do without interceptors in inner scopes altogether. As an alternative, a very generic interceptor in the root scope could be used. Such an interceptor may even load additional logic with a dynamic import from lazy applications parts.





Legacy Interceptors and Other Features


While the new functional interceptors are very charming, applications can still take advantage of the original class-based interceptors. This option can be enabled using the withLegacyInterceptors feature. Then, the class-based interceptors are to be registered as usual via a multi-provider:

1 bootstrapApplication(AppComponent, { 2 	providers: [ 3 		provideHttpClient( 4 			withInterceptors([authInterceptor]), 5 			withLegacyInterceptors(), 6 		), 7 		{ 8 			provide: HTTP_INTERCEPTORS, 9 			useClass: LegacyInterceptor, 10 			multiple: true, 11 		}, 12 	] 13 });





Further Features


The HttpClient has some additional features that can also be activated using with-functions: withJsonpSupport, for example, activates support for JSONP, and withXsrfConfiguration configures details on the use of XSRF tokens. If the application does not call withXsrfConfiguration, default settings are used. However, to completely disable the use of XSRF tokens, call withNoXsrfProtection.





Conclusion


The revised HttpClient now wonderfully harmonizes with standalone components and associated concepts such as environment injectors. The Angular team also took the opportunity to revise the interceptors: They can now be implemented in the form of simple functions. In addition, it is now also possible to consider interceptors in outer scopes.





Testing Angular Standalone Components


With Standalone Components, Angular becomes a lot more lightweight: NgModules are optional and hence we can work with lesser indirections. To make this possible, components now refer directly to their dependencies: other components, but also directives and pipes. There are so-called Standalone APIs for configuring services such as the HttpClient.

Additional Standalone APIs provide mocks for test automation. Here, I’m going to present these APIs. For this, I focus on on-board tools supplied with Angular. The 📂 examples used can be found here

If you don’t want to use the on-board resources alone, you will find the same examples based on the new Cypress Component Test Runner and on Testing Library in the third-party-testing branch.





Test Setup


Even though Standalone Components make modules optional, the TestBed still comes with a testing module. It takes care of the test setup and provides all components, directives, pipes, and services for the test:

1 import { provideHttpClient } from '@angular/common/http'; 2 import { HttpTestingController, provideHttpClientTesting } 3 from '@angular/common/http/testing'; 4 5 […] 6 7 describe('FlightSearchComponent', () => { 8 let component: FlightSearchComponent; 9 let fixture: ComponentFixture<FlightSearchComponent>; 10 beforeEach(async () => { 11 12 await TestBed.configureTestingModule({ 13 imports: [ FlightSearchComponent ], 14 providers: [ 15 provideHttpClient(), 16 provideHttpClientTesting(), 17 18 provideRouter([]), 19 20 provideStore(), 21 provideState(bookingFeature), 22 provideEffects(BookingEffects), 23 ], 24 }) 25 .compileComponents(); 26 27 fixture = TestBed.createComponent(FlightSearchComponent); 28 component = fixture.componentInstance; 29 fixture.detectChanges(); 30 }); 31 32 it('should search for flights', () => { […] }); 33 });



The example shown imports the Standalone Component to be tested and provides the required services via the providers array. This is exactly where the mentioned Standalone APIs come into play. They provide the services for the HttpClient, the router and NGRX.

The provideStore function sets up the NGRX store, provideState provides a feature slice required for the test, and provideEffects registers an associated effect. Below we will swap out these constructs for mocks.

The provideHttpClientTesting method is interesting: it overrides the HttpBackend used behind the scenes by the HttpClient with an HttpTestingBackend that simulates HTTP calls. It should be noted that it must be called after (!) provideHttpClient.

It is therefore first necessary to set up the HttpClient by default in order to then overwrite individual details for testing. This is a pattern we will see again below when testing the router.





The HttpClient Mock


Once the HttpClient and HttpTestingBackend have been set up, the individual tests are implemented as usual: The test uses the HttpTestingController to find out about pending HTTP requests and to specify the HTTP responses to be simulated:

1 it('should search for flights', () => { 2 component.from = 'Paris'; 3 component.to = 'London'; 4 component.search(); 5 6 const ctrl = TestBed.inject(HttpTestingController); 7 8 const req = ctrl.expectOne('https://[…]/flight?from=Paris&to=London'); 9 req.flush([{}, {}, {}]); // return 3 empty objects as dummy flights 10 11 component.flights$.subscribe(flights => { 12 expect(flights.length).toBe(3); 13 }); 14 15 ctrl.verify(); 16 });



The test then checks whether the component processed the simulated HTTP response as intended. In the case shown, the test assumes that the component offers the received flights via its flights property.

At the end, the test ensures that there are no further HTTP requests that have not yet been answered. To do this, it calls the verify method provided by the HttpTestingController. If there are still open requests at this point, verify throws an exception that causes the test to fail.





Shallow Testing


If you test a component, all sub-components, directives, and pipes used in the template are automatically tested as well. This is undesirable, especially for unit tests that focus on a single code unit. Also, this behavior slows down test execution when there are many dependencies.

Shallow tests are used to prevent this. This means that the test setup replaces all dependencies with mocks. These mocks must have the same interface as the replaced dependencies. In the case of components, this means – among other things – that the same properties and events (inputs and outputs) must be offered, but also that the same selectors must be used.

The TestBed offers the overrideComponent method for exchanging these dependencies:

1 await TestBed.configureTestingModule([…]) 2 .overrideComponent(FlightSearchComponent, { 3 remove: { imports: [ FlightCardComponent ] }, 4 add: { imports: [ FlightCardMock ] } 5 }) 6 .compileComponents();



In the case shown, the FlightSearchComponent uses another Standalone Component in its template: the FlightCardComponent. Technically, this means that the FlightCardComponent appears in the imports array of FlightSearchComponent. For implementing a shallow Test, this entry is removed. As a replacement, the FlightCardMock is added. The remove and add methods take care of this task.

The FlightSearchComponent is thus used in the test without real dependencies. Nevertheless, the test can check whether components behave as desired. For example, the following listing checks whether the FlightSearchComponent sets up an element named flight-card for each flight found.

1 it('should display a flight-card for each found flight', () => { 2 component.from = 'Paris'; 3 component.to = 'London'; 4 component.search(); 5 6 const ctrl = TestBed.inject(HttpTestingController); 7 8 const req = ctrl.expectOne('https://[…]/flight?from=Paris&to=London'); 9 req.flush([{}, {}, {}]); 10 11 fixture.detectChanges(); 12 13 const cards = fixture.debugElement.queryAll(By.css('flight-card')); 14 expect(cards.length).toBe(3); 15 });





Mock Router and Store


The test setup used so far only simulated the HttpCient. However, there are also Standalone APIs for mocking the router and NGRX:

1 import { provideRouter } from '@angular/router'; 2 import { provideLocationMocks } from '@angular/common/testing'; 3 4 import { provideMockStore } from '@ngrx/store/testing'; 5 import { provideMockActions } from '@ngrx/effects/testing'; 6 7 […] 8 9 describe('FlightSearchComponent (at router level)', () => { 10 let component: FlightSearchComponent; 11 let fixture: ComponentFixture<FlightSearchComponent>; 12 let actions$ = new Subject<Action>(); 13 14 beforeEach(async () => { 15 await TestBed.configureTestingModule({ 16 providers: [ 17 provideHttpClient(), 18 provideHttpClientTesting(), 19 20 provideRouter([ 21 { path: 'flight-edit/:id', component: FlightEditComponent } 22 ]), 23 provideLocationMocks(), 24 25 provideMockStore({ 26 initialState: { 27 [BOOKING_FEATURE_KEY]: { 28 flights: [{ id:1 }, { id:2 }, { id:3 }], 29 }, 30 }, 31 }), 32 33 provideMockActions(() => actions$), 34 ], 35 imports: [FlightSearchComponent], 36 }).compileComponents(); 37 38 fixture = TestBed.createComponent(FlightSearchComponent); 39 component = fixture.componentInstance; 40 fixture.detectChanges(); 41 }); 42 43 […] 44 });



As with testing the HttpClient, the test first sets up the router in the usual way. Then, it uses provideLocationMocks to override a couple of internally used services, namely Location and LocationStrategy. This procedure allows the route change to be simulated in the test cases. The MockStore which also ships with NGRX is used instead of the traditional one. It allows the entire content of the store to be freely defined. This is done either by calling provideMockStore or via its setState method. Also, provideMockActions gives us the ability to swap out the actions$ observable that NGRX effects often rely on. A test case using this setup could look like as follows:

1 it('routes to flight-card', fakeAsync(() => { 2 3 const link = fixture.debugElement.query(By.css('a[class*=btn-default ]')) 4 link.nativeElement.click(); 5 6 flush(); 7 fixture.detectChanges(); 8 9 const location = TestBed.inject(Location); 10 expect(location.path()).toBe('/flight-edit/1;showDetails=false') 11 12 }));



This test assumes that the FlightSearchComponent displays one link per flight in the (mock)store. It simulates a click on the first link and checks whether the application then switches to the expected route. In order for Angular to process the simulated click and trigger the route change, the change detection must be running. Unfortunately, this is not automatically the case with tests. Instead, it is to be triggered with the detectChanges method when required. The operations involved are asynchronous. Hence, fakeAsync is used so that the we don’t need to burdened ourselves with this. It allows pending micro-tasks to be processed synchronously using flush.## Testing Effects

The MockStore does not trigger reducers or effects. The former are just functions and can be tested in a straight forward way. Replacing action$ is a good way to test effects. The test setup in the last section has already taken care of that. A test based on this could now use the observable action$ to send an action to which the tested effect reacts:

1 it('load flights', () => { 2 const effects = TestBed.inject(BookingEffects); 3 let flights: Flight[] = []; 4 5 effects.loadFlights$.subscribe(action => { 6 flights = action.flights; // Action returned from Effect 7 }); 8 9 actions$.next(loadFlights({ from: 'Paris', to: 'London' })); 10 // Action sent to store to invoke Effect 11 12 const ctrl = TestBed.inject(HttpTestingController); 13 const req = ctrl.expectOne('https://[…]/flight?from=Paris&to=London'); 14 req.flush([{}, {}, {}]); 15 16 expect(flights.length).toBe(3); 17 });



In the case under consideration, the effect triggers an HTTP call answered by the HttpTestingController. The response contains three flights, represented by three empty objects for the sake of simplicity. Finally, the test checks whether the effect provided these flights via the outbound action.





Conclusion


More and more libraries offer Standalone APIs for mocking dependencies. These either provide a mock implementation or at least overwrite services in the actual implementation to increase testability. The TestingModule is still used to provide the test setup. Unlike before, however, it now imports the standalone components, directives, and pipes to be tested. Their classic counterparts, on the other hand, were declared. In addition, the TestingModule now includes providers setup by Standalone APIs.





Patterns for Custom Standalone APIs in Angular


Together with Standalone Components, the Angular team introduced Standalone APIs. They allow for setting up libraries in a more lightweight way. Examples of libraries currently providing Standalone APIs are the HttpClient and the Router. Also, NGRX is an early adopter of this idea.

In this chapter, I present several patterns for writing custom Standalone APIs inferred from the before mentioned libraries. For each pattern, the following aspects are discussed: intentions behind the pattern, description, example implementation, examples for occurrences in the mentioned libraries, and variations for implementation details.

Most of these patterns are especially interesting for library authors. They have the potential to improve the DX for the library’s consumers. On the other side, most of them might be overkill for applications.

Big thanks to Angular’s <PRESIDIO_ANONYMIZED_PERSON> for proofreading and providing feedback!



📂 Source code used in examples





Case Study for Patterns


For presenting the inferred patterns, a simple logger library is used. This library is as simple as possible but as complex as needed to demonstrate the implementation of the patterns:

<img src=”images/logger.png” style=”max-width:300px”>

Each log message has a LogLevel, defined by an enum:

1 export enum LogLevel { 2 DEBUG = 0, 3 INFO = 1, 4 ERROR = 2, 5 }



For the sake of simplicity, we restrict our Logger library to just three log levels.

An abstract LoggerConfig defines the possible configuration options:

1 export abstract class LoggerConfig { 2 abstract level: LogLevel; 3 abstract formatter: Type<LogFormatter>; 4 abstract appenders: Type<LogAppender>[]; 5 }



It’s an abstract class on purpose, as interfaces cannot be used as tokens for DI. A constant of this class type defines the default values for the configuration options:

1 export const defaultConfig: LoggerConfig = { 2 level: LogLevel.DEBUG, 3 formatter: DefaultLogFormatter, 4 appenders: [DefaultLogAppender], 5 };



The LogFormatter is used for formatting log messages before they are published via a LogAppender:

1 export abstract class LogFormatter { 2 abstract format(level: LogLevel, category: string, msg: string): string; 3 }



Like the LoggerConfiguration, the LogFormatter is an abstract class used as a token. The consumer of the logger lib can adjust the formatting by providing its own implementation. As an alternative, they can go with a default implementation provided by the lib:

1 @Injectable() 2 export class DefaultLogFormatter implements LogFormatter { 3 format(level: LogLevel, category: string, msg: string): string { 4 const levelString = LogLevel[level].padEnd(5); 5 return `[${levelString}] ${category.toUpperCase()} ${msg}`; 6 } 7 }



The LogAppender is another exchangeable concept responsible for appending the log message to a log:

1 export abstract class LogAppender { 2 abstract append(level: LogLevel, category: string, msg: string): void; 3 }



The default implementation writes the message to the console:

1 @Injectable() 2 export class DefaultLogAppender implements LogAppender { 3 append(level: LogLevel, category: string, msg: string): void { 4 console.log(category + ' ' + msg); 5 } 6 }



While there can only be one LogFormatter, the library supports several LogAppenders. For instance, a first LogAppender could write the message to the console while a second one could also send it to the server.

To make this possible, the individual LogAppenders are registered via multi providers. Hence, the Injector returns all of them within an array. As an array cannot be used as a DI token, the example uses an InjectionToken instead:

1 export const LOG_APPENDERS = new InjectionToken<LogAppender[]>("LOG_APPENDERS");



The LoggserService itself receives the LoggerConfig, the LogFormatter, and an array with LogAppenders via DI and allows to log messages for several LogLevels:

1 @Injectable() 2 export class LoggerService { 3 private config = inject(LoggerConfig); 4 private formatter = inject(LogFormatter); 5 private appenders = inject(LOG_APPENDERS); 6 7 log(level: LogLevel, category: string, msg: string): void { 8 if (level < this.config.level) { 9 return; 10 } 11 const formatted = this.formatter.format(level, category, msg); 12 for (const a of this.appenders) { 13 a.append(level, category, formatted); 14 } 15 } 16 17 error(category: string, msg: string): void { 18 this.log(LogLevel.ERROR, category, msg); 19 } 20 21 info(category: string, msg: string): void { 22 this.log(LogLevel.INFO, category, msg); 23 } 24 25 debug(category: string, msg: string): void { 26 this.log(LogLevel.DEBUG, category, msg); 27 } 28 }





The Golden Rule


Before I start with presenting the inferred patterns, I want to stress out what I call the golden rule for providing services:

Whenever possible, use @Injectable({providedIn: 'root'})!



Especially in application code but in several situations in libraries, this is what you want to have: It’s easy, tree-shakable, and even works with lazy loading. The latter aspect is less a merit of Angular than the underlying bundler: Everything that is just needed in a lazy bundle is put there.





Pattern: Provider Factory


Intentions


Providing services for a reusable lib

Configuring a reusable lib

Exchanging defined implementation details





Description


A Provider Factory is a function returning an array with providers for a given library. This Array is cross-casted into Angular’s EnvironmentProviders type to make sure the providers can only be used in an environment scope – first and foremost, the root scope and scopes introduced with lazy routing configurations.

Angular and NGRX place such functions in files called provider.ts.





Example


The following Provider Function provideLogger takes a partial LoggerConfiguration and uses it to create some providers:

1 export function provideLogger( 2 config: Partial<LoggerConfig> 3 ): EnvironmentProviders { 4 // using default values for missing properties 5 const merged = { ...defaultConfig, ...config }; 6 7 return makeEnvironmentProviders([ 8 { 9 provide: LoggerConfig, 10 useValue: merged, 11 }, 12 { 13 provide: LogFormatter, 14 useClass: merged.formatter, 15 }, 16 merged.appenders.map((a) => ({ 17 provide: LOG_APPENDERS, 18 useClass: a, 19 multi: true, 20 })), 21 ]); 22 }



Missing configuration values are taken from the default configuration. Angular’s makeEnvironmentProviders wraps the Provider array in an instance of EnvironmentProviders.

This function allows the consuming application to setup the logger during bootstrapping like other libraries, e. g. the HttpClient or the Router:

1 bootstrapApplication(AppComponent, { 2 providers: [ 3 4 provideHttpClient(), 5 6 provideRouter(APP_ROUTES), 7 8 [...] 9 10 // Setting up the Logger: 11 provideLogger(loggerConfig), 12 ] 13 }





Occurrences and Variations


This is a usual pattern used in all examined libraries

The Provider Factories for the Router and HttpClient have a second optional parameter that takes additional features (see Pattern Feature, below).

Instead of passing in the concrete service implementation, e. g. LogFormatter, NGRX allows taking either a token or the concrete object for reducers.

The HttpClient takes an array with functional interceptors via a with function (see Pattern Feature, below). These functions are also registered as services.





Pattern: Feature


Intentions


Activating and configuring optional features

Making these features tree-shakable

Providing the underlying services via the current environment scope





Description


The Provider Factory takes an optional array with a feature object. Each feature object has an identifier called kind and a providers array. The kind property allows for validating the combination of passed features. For instance, there might be mutually exclusive features like configuring XSRF token handling and disabling XSRF token handling for the HttpClient.





Example


Our example uses a color feature that allows for displaying messages of different LoggerLevels in different colors:

<img src=”images/feature.png” style=”max-width:300px”>

For categorizing features, an enum is used:

1 export enum LoggerFeatureKind { 2 COLOR, 3 OTHER_FEATURE, 4 ADDITIONAL_FEATURE 5 }



Each feature is represented by an object of LoggerFeature:

1 export interface LoggerFeature { 2 kind: LoggerFeatureKind; 3 providers: Provider[]; 4 }



For providing the color feature, a factory function following the naming pattern withFeature is introduced:

1 export function withColor(config?: Partial<ColorConfig>): LoggerFeature { 2 const internal = { ...defaultColorConfig, ...config }; 3 4 return { 5 kind: LoggerFeatureKind.COLOR, 6 providers: [ 7 { 8 provide: ColorConfig, 9 useValue: internal, 10 }, 11 { 12 provide: ColorService, 13 useClass: DefaultColorService, 14 }, 15 ], 16 }; 17 }



The Provider Factory takes several features via an optional second parameter defined as a rest array:

1 export function provideLogger( 2 config: Partial<LoggerConfig>, 3 ...features: LoggerFeature[] 4 ): EnvironmentProviders { 5 const merged = { ...defaultConfig, ...config }; 6 7 // Inspecting passed features 8 const colorFeatures = 9 features?.filter((f) => f.kind === LoggerFeatureKind.COLOR)?.length ?? 0; 10 11 // Validating passed features 12 if (colorFeatures > 1) { 13 throw new Error("Only one color feature allowed for logger!"); 14 } 15 16 return makeEnvironmentProviders([ 17 { 18 provide: LoggerConfig, 19 useValue: merged, 20 }, 21 { 22 provide: LogFormatter, 23 useClass: merged.formatter, 24 }, 25 merged.appenders.map((a) => ({ 26 provide: LOG_APPENDERS, 27 useClass: a, 28 multi: true, 29 })), 30 31 // Providing services for the features 32 features?.map((f) => f.providers), 33 ]); 34 }



The kind property of the feature is used to examine and validate the passed features. If everything is fine, the providers found in the feature are put into the returned EnvironmentProviders object.

The DefaultLogAppender gets hold of the ColorService provided by the color feature via dependency injection:

1 export class DefaultLogAppender implements LogAppender { 2 colorService = inject(ColorService, { optional: true }); 3 4 append(level: LogLevel, category: string, msg: string): void { 5 if (this.colorService) { 6 msg = this.colorService.apply(level, msg); 7 } 8 console.log(msg); 9 } 10 }



As features are optional, the DefaultLogAppender passes optional: true to inject. Otherwise, we would get an exception if the feature is not applied. Also, the DefaultLogAppender needs to check for null values.





Occurrences and Variations


The Router uses it, e. g. for configuring preloading or for activating debug tracing.

The HttpClient uses it, e. g. for providing interceptors, configuring JSONP, and configuring/ disabling the XSRF token handling

Both, the Router and the HttpClient, combine the possible features to a union type (e.g. export type AllowedFeatures = ThisFeature | ThatFeature). This helps IDEs to propose built-in features.

Some implementations inject the current Injector and use it to find out which features have been configured. This is an imperative alternative to using optional: true.

Angular’s feature implementations prefix the properties kind and providers with ɵ and hence declare them as internal properties.





Pattern: Configuration Provider Factory


Intentions


Configuring existing services

Providing additional services and registering them with existing services

Extending the behavior of a service from within a nested environment scope





Description


Configuration Provider Factories extend the behavior of an existing service. They may provide additional services and use an ENVIRONMENT_INITIALIZER to get hold of instances of both the provided services as well as the existing services to extend.





Example


Let’s assume an extended version of our LoggerService that allows for defining an additional LogAppender for each log category:

1 @Injectable() 2 export class LoggerService { 3 4 private appenders = inject(LOG_APPENDERS); 5 private formatter = inject(LogFormatter); 6 private config = inject(LoggerConfig); 7 [...] 8 9 // Additional LogAppender per log category 10 readonly categories: Record<string, LogAppender> = {}; 11 12 log(level: LogLevel, category: string, msg: string): void { 13 14 if (level < this.config.level) { 15 return; 16 } 17 18 const formatted = this.formatter.format(level, category, msg); 19 20 // Lookup appender for this very category and use 21 // it, if there is one: 22 const catAppender = this.categories[category]; 23 24 if (catAppender) { 25 catAppender.append(level, category, formatted); 26 } 27 28 // Also, use default appenders: 29 for (const a of this.appenders) { 30 a.append(level, category, formatted); 31 } 32 33 } 34 35 [...] 36 }



To configurate a LogAppender for a category, we can introduce another Provider Factory:

1 export function provideCategory( 2 category: string, 3 appender: Type<LogAppender> 4 ): EnvironmentProviders { 5 // Internal/ Local token for registering the service 6 // and retrieving the resolved service instance 7 // immediately after. 8 const appenderToken = new InjectionToken<LogAppender>("APPENDER_" + category); 9 10 return makeEnvironmentProviders([ 11 { 12 provide: appenderToken, 13 useClass: appender, 14 }, 15 { 16 provide: ENVIRONMENT_INITIALIZER, 17 multi: true, 18 useValue: () => { 19 const appender = inject(appenderToken); 20 const logger = inject(LoggerService); 21 22 logger.categories[category] = appender; 23 }, 24 }, 25 ]); 26 }



This factory creates a provider for the LogAppender class. However, we don’t need the class but rather an instance of it. Also, we need the Injector to resolve this instance’s dependencies. Both happen when retrieving a LogAppender via inject.

Precisely this is done by the ENVIRONMENT_INITIALIZER, which is multi provider bound to the token ENVIRONMENT_INITIALIZER and pointing to a function. It gets the LogAppender injected but also the LoggerService. Then, the LogAppender is registered with the logger.

This allows for extending the existing LoggerService that can even come from a parent scope. For instance, the following example assumes the LoggerService in the root scope while the additional log category is setup in the scope of a lazy route:

1 export const FLIGHT_BOOKING_ROUTES: Routes = [ 2 { 3 path: '', 4 component: FlightBookingComponent, 5 6 // Providers for this route and child routes 7 // Using the providers array sets up a new 8 // environment injector for this part of the 9 // application. 10 providers: [ 11 // Setting up an NGRX feature slice 12 provideState(bookingFeature), 13 provideEffects([BookingEffects]), 14 15 // Provide LogAppender for logger category 16 provideCategory('booking', DefaultLogAppender), 17 ], 18 children: [ 19 { 20 path: 'flight-search', 21 component: FlightSearchComponent, 22 }, 23 [...] 24 ], 25 }, 26 ];





Occurrences and Variations


@ngrx/store uses this pattern to register feature slices

@ngrx/effects uses this pattern, to wire-up effects provided by a feature

The feature withDebugTracing uses this pattern to subscribe to the Router’s events Observable.





Pattern: NgModule Bridge


Intentions


Not breaking existing code using NgModules when switching to Standalone APIs.

Allowing such application parts to set up EnvironmentProviders that come from a Provider Factory.



Remarks: For new code, this pattern seems to be overkill, because the Provider Factory can be directly called for the consuming (legacy) NgModules.





Description


The NgModule Bridge is a NgModule deriving (some of) its providers via a Provider Factory (see pattern Provider Factory). To give the caller more control over the provided services, static methods like forRoot can be used. These methods can take a configuration object.





Example


The following NgModules allows for setting up the Logger in a traditional way:

1 @NgModule({ 2 imports: [/* your imports here */], 3 exports: [/* your exports here */], 4 declarations: [/* your delarations here */], 5 providers: [/* providers, you _always_ want to get, here */], 6 }) 7 export class LoggerModule { 8 static forRoot(config = defaultConfig): ModuleWithProviders<LoggerModule> { 9 return { 10 ngModule: LoggerModule, 11 providers: [ 12 provideLogger(config) 13 ], 14 }; 15 } 16 17 static forCategory( 18 category: string, 19 appender: Type<LogAppender> 20 ): ModuleWithProviders<LoggerModule> { 21 return { 22 ngModule: LoggerModule, 23 providers: [ 24 provideCategory(category, appender) 25 ], 26 }; 27 } 28 }



To avoid reimplementing the Provider Factories, the Module’s methods delegate to them. As using such methods is usual when working with NgModules, consumers can leverage existing knowledge and conventions.





Occurrences and Variations


All the examined libraries use this pattern to stay backwards compatible





Pattern: Service Chain


Intentions


Making a service delegating to another instance of itself in a parent scope.





Description


When the same service is placed in several nested environment injectors, we normally only get the service instance of the current scope. Hence, a call to the service in a nested scope is not respected in the parent scope. To work around this, a service can look up an instance of itself in the parent scope and delegate to it.





Example


Let’s assume we provide the logger library again for a lazy route:

1 export const FLIGHT_BOOKING_ROUTES: Routes = [ 2 { 3 path: '', 4 component: FlightBookingComponent, 5 canActivate: [() => inject(AuthService).isAuthenticated()], 6 providers: [ 7 // NGRX 8 provideState(bookingFeature), 9 provideEffects([BookingEffects]), 10 11 // Providing **another** logger for this part of the app: 12 provideLogger( 13 { 14 level: LogLevel.DEBUG, 15 chaining: true, 16 appenders: [DefaultLogAppender], 17 }, 18 withColor({ 19 debug: 42, 20 error: 43, 21 info: 46, 22 }) 23 ), 24 25 ], 26 children: [ 27 { 28 path: 'flight-search', 29 component: FlightSearchComponent, 30 }, 31 [...] 32 ], 33 }, 34 ];



This sets up another set of the Logger’s services in the environment injector of this lazy route and its children. These services are shadowing their counterparts in the root scope. Hence, when a component in the lazy scope calls the LoggerService, the services in the root scope are not triggered.

To prevent this, we can get the LoggerService from the parent scope. More precisely, it’s not the parent scope but the “nearest ancestor scope” providing a LoggerService. After that, the service can delegate to its parent. This way, the services are chained:

1 @Injectable() 2 export class LoggerService { 3 private appenders = inject(LOG_APPENDERS); 4 private formatter = inject(LogFormatter); 5 private config = inject(LoggerConfig); 6 7 private parentLogger = inject(LoggerService, { 8 optional: true, 9 skipSelf: true, 10 }); 11 [...] 12 13 log(level: LogLevel, category: string, msg: string): void { 14 15 // 1. Do own stuff here 16 [...] 17 18 // 2. Delegate to parent 19 if (this.config.chaining && this.parentLogger) { 20 this.parentLogger.log(level, category, msg); 21 } 22 } 23 [...] 24 }



When using inject to get hold of the parent’s LoggerService, we need to pass the optional: true to avoid an exception if there is no ancestor scope with a LoggerService. Passing skipSelf: true makes sure, only ancestor scopes are searched. Otherwise, Angular would start with the current scope and retrieve the calling service itself.

Also, the example shown here allows activating/deactivating this behavior via a new chaining flag in the LoggerConfiguration.





Occurrences and Variations


The HttpClient uses this pattern to also trigger HttpInterceptors in parent scopes. More details on chaining HttpInterceptors can be found here. Here, the chaining behavior can be activated via a separate feature. Technically, this feature registers another interceptor delegating to services in the parent scope.





Pattern: Functional Service


Intentions


Making the usage of libraries more lightweight by using functions as services

Reducing indirections by going with ad-hoc functions





Description


Instead of forcing the consumer to implement a class-based service following a given interface, a library also accepts functions. Internally, they can be registered as a service using useValue.





Example


In this example, the consumer can directly pass a function acting as a LogFormatter to provideLogger:

1 bootstrapApplication(AppComponent, { 2 providers: [ 3 provideLogger( 4 { 5 level: LogLevel.DEBUG, 6 appenders: [DefaultLogAppender], 7 8 // Functional CSV-Formatter 9 formatter: (level, cat, msg) => [level, cat, msg].join(";"), 10 }, 11 withColor({ 12 debug: 3, 13 }) 14 ), 15 ], 16 });



To allow for this, the Logger uses a LogFormatFn type defining the function’s signature:

1 export type LogFormatFn = ( 2 level: LogLevel, 3 category: string, 4 msg: string 5 ) => string;



Also, as functions cannot be used as tokens, an InjectionToken is introduced:

1 export const LOG_FORMATTER = new InjectionToken<LogFormatter | LogFormatFn>( 2 "LOG_FORMATTER" 3 );



This InjectionToken supports both class-based LogFormatter as well as functional ones. This prevents breaking existing code. As a consequence of supporting both, provideLogger needs to treat both cases in a slightly different way:

1 export function provideLogger(config: Partial<LoggerConfig>, ...features: LoggerFeat\ 2 ure[]): EnvironmentProviders { 3 4 const merged = { ...defaultConfig, ...config}; 5 6 [...] 7 8 return makeEnvironmentProviders([ 9 LoggerService, 10 { 11 provide: LoggerConfig, 12 useValue: merged 13 }, 14 15 // Register LogFormatter 16 // - Functional LogFormatter: useValue 17 // - Class-based LogFormatters: useClass 18 (typeof merged.formatter === 'function' ) ? { 19 provide: LOG_FORMATTER, 20 useValue: merged.formatter 21 } : { 22 provide: LOG_FORMATTER, 23 useClass: merged.formatter 24 }, 25 26 merged.appenders.map(a => ({ 27 provide: LOG_APPENDERS, 28 useClass: a, 29 multi: true 30 })), 31 [...] 32 ]); 33 }



While class-based services are registered with useClass, useValue is the right choice for their functional counterparts.

Also, the consumers of the LogFormatter need to be prepared for both the functional as well as class-based approach:

1 @Injectable() 2 export class LoggerService { 3 private appenders = inject(LOG_APPENDERS); 4 private formatter = inject(LOG_FORMATTER); 5 private config = inject(LoggerConfig); 6 7 [...] 8 9 private format(level: LogLevel, category: string, msg: string): string { 10 if (typeof this.formatter === 'function') { 11 return this.formatter(level, category, msg); 12 } 13 else { 14 return this.formatter.format(level, category, msg); 15 } 16 } 17 18 log(level: LogLevel, category: string, msg: string): void { 19 if (level < this.config.level) { 20 return; 21 } 22 23 const formatted = this.format(level, category, msg); 24 25 [...] 26 } 27 [...] 28 }





Occurrences and Variations


The HttpClient allows using functional interceptors. They are registered via a feature (see pattern Feature).

The Router allows using functions for implementing guards and resolvers.





Conclusion


Provider Factories are simple functions returning an array with providers. They are used to get all providers needed for setting up a subsystem or a library. By convention, such factories follow the naming pattern privateXY.

A provider factory can accept a configuration object and optional features. An optional feature is another function returning all providers needed for the feature in question. Their names follow the naming pattern withXYZ.

An ENVIRONMENT_INITIALIZER can be used to wire up services and using the inject together with parameters like optional and skipSelf allows for establishing a chain with another instance of the same service in a parent scope.





How to prepare for Standalone Components?


After getting started with Standalone Components the question arises how to migrate an existing Angular solution for a future without Angular modules. In this chapter I show four options to do so.





Option 1: Ostrich Strategy


Let’s start with the simplest option - the ostrich strategy. Stick your head in the sand and ignore everything around you:

Ostrich sticking its head into the sand

Even if that sounds smug, there is actually nothing wrong with it. Nobody is forcing us to convert applications to Standalone Components. Angular will continue to support Angular modules. After all, the entire ecosystem is based on it. You can therefore safely ignore Standalone Components or only use this new option in new applications or application parts.





Option 2: Just Throw Away Angular Modules


This strategy also seems smug at first glance: You simply remove all Angular modules from your source code. This doesn’t have to be done in one go either, because Standalone Components play wonderfully together with Angular modules. Angular modules can be imported into Standalone Components and vice versa.

For instance, the following listing shows a Standalone Component importing further NgModules:

1 import { Component, OnInit } from '@angular/core'; 2 import { TicketsModule } from '../tickets/tickets.module'; 3 4 @Component({ 5 selector: 'app-next-flight', 6 standalone: true, 7 imports: [ 8 // Existing NgModule imported 9 // in this standalone component 10 TicketsModule 11 ], 12 [...] 13 }) 14 export class NextFlightComponent implements OnInit { 15 [...] 16 }



To illustrate the other way round, this listing shows an NgModule importing a Standalone Component:

1 @NgModule({ 2 imports: [ 3 CommonModule, 4 5 // Imported Standalone Component: 6 FlightCardComponent, 7 [...] 8 ], 9 declarations: [ 10 MyTicketsComponent 11 ], 12 [...] 13 }) 14 export class TicketsModule { }



This mutual compatibility is made possible by the mental model behind Standalone Components.

Accordingly, a standalone component is a combination of a component and a module. Even if the actual technical implementation does not set up any dedicated Angular modules, this idea helps to bridge the gap between the two worlds. It also explains why Angular modules and Standalone Components can import each other.

If you go with this strategy, you need to import the compilation context directly into the Standalone Component using its imports array. I like to think about this compilation context as about the component’s neighborhood: It contains all other Standalone Components, Standalone Directives, and Standalone Pipes but also NgModules the component in question needs.





Option 2a: Automatic Migration to Standalone


The Angular CLI helps with migrating an existing code base to Standalone Components. The next chapter will discuss this option in detail.





Option 3: Replace Angular Modules with Barrels


Barrels are EcmaScript files that (re)export related building blocks:

1 import { NavbarComponent } from './navbar/navbar.component'; 2 import { SidebarComponent } from './sidebar/sidebar.component';



The consumer can now import everything the barrel provides:

1 import { NavbarComponent, SidebarComponent } as shell from '../shell';



If the barrel is called index.ts, it is sufficient to import only the barrel folder. In addition to grouping, this approach also has the advantage that barrels can be used to define public APIs: All building blocks exported by the barrel can be used by other parts of the application. They just need to import from the barrel. Everything else is considered an implementation detail that should not be accessed by other application parts. Hence, such implementation details are quite easy to change without producing breaking changes somewhere else. This is a simple but effective measure for stable software architectures.

In a further step, each barrel could also receive a path mapping in the tsconfig.json. In this case, the application can access the barrel using nice names similar to npm package names:

1 import { NavbarComponent, SidebarComponent } from '@demo/shell';



However, barrels also come with challenges: For example, they are often the cause of cyclical dependencies:

Cyclic dependencies via barrel

Here, b.ts on the one hand is referenced by the barrel index.ts and on the other hand accesses the barrel.

This problem can be avoided from the start with two simple rules that must be followed consistently:

A barrel may only publish elements from its “area”. The “area” extends over the barrel’s folder as well as its subfolders.

Within each “area”, files reference each other using relative paths without using the barrel.



Although these rules sound a bit abstract at first glance, the implementation of this rule is easier than you would think:

Avoiding cyclic dependencies

Here, b.ts directly accesses a.ts located in the same “area” to avoid the cycle shown earlier. The detour the barrel is avoided.

Another disadvantage is that each part of the program can bypass the specified barrels - and thus the public API created with them. Relative paths to private parts of the respective “areas” are sufficient for this.

This problem can be solved with linting. A linting rule would have to detect and denounce unauthorized access. The popular tool Nx comes with such a rule, which can also be used to prevent other unwanted accesses. The next section takes up this idea.





Option 4: Nx Workspace with Libraries and Linting Rules


The popular tool Nx is based on the Angular CLI and brings a lot of convenience for developing enterprise-scale solutions. Nx allows a large project to be broken down into different applications and libraries. Each library has a public API that specifies a barrel named index.ts. Nx also provides path mapping for all libraries. In addition, Nx brings a linting rule that prevents bypassing the barrel and also allows other restrictions.

This linting rule allows enforcing a fixed frontend architecture. For example, the Nx team recommends dividing a large application vertically by subject domains and horizontally by technical library categories:

Architecture Matrix

Feature libraries contain smart components that implement use cases, while UI libraries house reusable dump components. Domain libraries encapsulate the client-side domain model and services that operate on it, and utility libraries group general utility functions.

With the linting rules mentioned, it can now be ensured that each layer may only access the layers below it. Access to other domains can also be prevented. Libraries from the Booking area are therefore not allowed to access libraries in Boarding. If you want to use certain constructs across domains, they should be placed in the shared area, for example.

If someone violates one of these rules, the linter gives instant feedback:

Linting Rule Feedback

The folder structure used for this by Nx reflects the architecture matrix shown:

Structure of Nx workspace

The subfolders in libs represent the domains. The libraries found in it get a prefix like feature- or domain-. These prefixes reflect the technical categories and thus the layers.

The nice thing about this fourth option is that it has long proven itself in interaction with Angular modules for structuring large solutions:

Nx libs with NgModules

Thanks to Standalone Components, the Angular modules can be omitted now:

Nx libs without NgModules

In this case, only the libraries are used for structuring: their barrels group related building blocks, such as Standalone Components, and thanks to the linting rules mentioned, we can enforce our architectures.





Option 4a: Folder-based Module Boundaries with Sheriff


As discussed in one of the previous chapters, the open source tool Sheriff allows to enforce module boundaries on a per-folder basis. This makes the application structure more lightweight. It can be used with the Angular CLI but also with Nx to get the best of both worlds.





Conclusion


While Standalone Component are meanwhile the preferred way of using components, the traditional NgModule-based style is still a first-class citizen. Hence, we don’t need to migrate existing code immediately.

As both options play well together, we can mix and match them. For instance, we could sick with NgModules for existing code and write new code using Standalone Components.

Barrels and Libraries can be used as a replacement for NgModule. They allow for hiding implementation details regardless of whether they are Angular-based or not. With tools like Nx or Sheriff we can prevent the usage of such implementation details and define which parts of the application can access which other parts.





Automatic Migration to Standalone Components in 3 Steps


While the new Standalone Components can be perfectly combined with traditional NgModule-based Angular code, people might want to fully migrate their projects to the new standalone world. Since Angular 15.2, there is a schematic that automates this task. In 3 steps it converts projects. After each step, we can check the current progress and manually take care of details the automatic process could not cope with.

In this short tutorial, I’m going to step through these 3 steps with you and migrate our demo app.

If you want to play through these steps, you can find the NgModule-based initial situation of our tutorial here:

📂 https://github.com/manfredsteyer/standalone-example-cli <br> (Branch ngmodules)





A First Look at the Application to Migrate


After checking out the ngmodules branch (!) of the above-mentioned project, it might be a good idea to go through the source code a bit. You should recognize the following NgModules:

1 +----> SharedModule < --------+ 2 | | 3 AppModule --- (lazy) ---> FlightBookingModule



Also, start the application to get a first impression of it:

1 ng serve -o





Step 1


Now, let’s run the migration schematic available from Angular 15.2:

1 ng g @angular/core:standalone



When asked about the type of migration, we select the first option (because it’s such a nice tradition to “start at the beginning” …).



When asked about the path to migrate, we go with the default value by pressing enter:



This default value ./ points to the project’s root. Hence, the whole project will be migrated at once. For small and medium-sized applications this should be fine. For bigger applications, however, it might be interesting to migrate the project gradually.

After this first step, you should have a look at the source code and check if everything is fine. In the case of this example project, you don’t need to bother. The schematics do a great job here!





Step 2


Now, let’s run the schematic again for the second step:



The output shows that the SharedModule was removed and the other modules have been updated. It’s fine that the AppModule is still in place – it will be removed in the 3rd step. However, all other NgModules should be gone by now. Unfortunately, the FlightBookingModule is still here:

1 // src/app/booking/flight-booking.module.ts 2 3 @NgModule({ 4 imports: [ 5 CommonModule, 6 FormsModule, 7 StoreModule.forFeature(bookingFeature), 8 EffectsModule.forFeature([BookingEffects]), 9 RouterModule.forChild(FLIGHT_BOOKING_ROUTES), 10 FlightCardComponent, 11 FlightSearchComponent, 12 FlightEditComponent, 13 PassengerSearchComponent 14 ], 15 exports: [], 16 providers: [] 17 }) 18 export class FlightBookingModule { }



As this listing shows, the FlightBookingModule doesn’t do much anymore. However, there are some calls to methods within the imports section. These methods are for setting up the router and the NGRX store. As they are quite library-specific, the schematic was not able to convert them into calls of equivalent Standalone APIs. So, we need to take care of this by hand.

RouterModule.forChild sets up some child routes that are loaded alongside the FlightBookingModule. However, in a standalone world, we don’t need NgModules for setting up child routes anymore. Instead, the parent routing configuration can directly point to the child routes. Hence, let’s switch to the file app.routes.ts and update the route triggering lazy loading:

1 // src/app/app.routes.ts 2 3 { 4 path: 'flight-booking', 5 canActivate: [() => inject(AuthService).isAuthenticated()], 6 loadChildren: () => 7 import('./booking/flight-booking.routes') 8 .then(m => m.FLIGHT_BOOKING_ROUTES) 9 },



Please note that the import now directly imports the flight booking routes. There is no indirection via the FlightBookingModule anymore. There is even the possibility of shortening this further: If the file flight-booking.routes.ts exports the routes as its default export, we can skip the subsequent then call:

1 { 2 path: 'flight-booking', 3 canActivate: [() => inject(AuthService).isAuthenticated()], 4 loadChildren: () => 5 import('./booking/flight-booking.routes') 6 },



To make sure, the NGRX store is initialized for this lazy application part, we can register the respective providers directly for the lazy child routes:

1 // src/app/booking/flight-booking.routes.ts 2 import { importProvidersFrom, inject } from '@angular/core'; 3 [...] 4 5 export const FLIGHT_BOOKING_ROUTES: Routes = [ 6 { 7 path: '', 8 component: FlightBookingComponent, 9 canActivate: [() => inject(AuthService).isAuthenticated()], 10 providers: [ 11 importProvidersFrom(StoreModule.forFeature(bookingFeature)), 12 importProvidersFrom(EffectsModule.forFeature([BookingEffects])), 13 ], 14 children: [ 15 [...] 16 ], 17 }, 18 ];



This new provider array sets up services that are only needed for the route at hand and its children. The function importProvidersFrom bridges over to the world of NgModules and allows retrieving their providers.

Now, we can delete the FlightBookingModule (src/app/booking/flight-booking.module.ts).





Step 3


Let’s run our migration schematic for the 3rd time:



This removes the AppModule and updates the file main.ts to directly bootstrap the AppComponent. After this step, the application should work as before:

1 ng serve -o





Bonus: Moving to Standalone APIs


When we look into the main.ts, we see that it still references several modules with importProvidersFrom:

1 bootstrapApplication(AppComponent, { 2 providers: [ 3 importProvidersFrom( 4 BrowserModule, 5 LayoutModule, 6 LoggerModule.forRoot({ 7 level: LogLevel.DEBUG, 8 appenders: [DefaultLogAppender], 9 formatter: (level, cat, msg) => [level, cat, msg].join(';'), 10 }), 11 StoreModule.forRoot(reducer), 12 EffectsModule.forRoot(), 13 StoreDevtoolsModule.instrument(), 14 MatToolbarModule, 15 MatButtonModule, 16 MatSidenavModule, 17 MatIconModule, 18 MatListModule 19 ), 20 { 21 provide: HTTP_INTERCEPTORS, 22 useClass: LegacyInterceptor, 23 multi: true, 24 }, 25 provideAnimations(), 26 provideHttpClient(withInterceptorsFromDi()), 27 provideRouter(APP_ROUTES, withPreloading(PreloadAllModules)), 28 ], 29 });



Also, there is a traditional class-based HttpInterceptor registered and the HttpClient is made aware of this by calling withInterceptorsFromDi. By moving to Standalone APIs, this can be improved:

1 bootstrapApplication(AppComponent, { 2 providers: [ 3 4 provideLogger({ 5 level: LogLevel.DEBUG, 6 appenders: [DefaultLogAppender], 7 formatter: (level, cat, msg) => [level, cat, msg].join(';'), 8 }), 9 10 provideStore(reducer), 11 provideEffects(), 12 provideStoreDevtools(), 13 14 provideAnimations(), 15 16 provideHttpClient(withInterceptors([authInterceptor])), 17 provideRouter(APP_ROUTES, withPreloading(PreloadAllModules)), 18 19 importProvidersFrom( 20 LayoutModule, 21 MatToolbarModule, 22 MatButtonModule, 23 MatSidenavModule, 24 MatIconModule, 25 MatListModule 26 ), 27 28 ], 29 });



This modification takes care of the following aspects:

Removing the import of the BrowserModule that doesn’t need to be explicitly imported when bootstrapping a Standalone Component.

Setting up the custom Logger library with provideLogger.

Setting up the NGRX store with provideStore, provideEffects, and provideStoreDevtools.

Replacing the traditional HttpInterceptor with a functional interceptor that is now passed to withInterceptors. To make this step easier, the functional counterpart has already been part of the code base since the beginning.



More information about custom Standalone APIs like provideLogger can be found here.

NGRX expects that its Standalone APIs are used fully or not at all. Hence, we also need to go back to the flight-booking.routes.ts and replace the call to importProvidersFrom with calls to provideState and provideEffects:

1 export const FLIGHT_BOOKING_ROUTES: Routes = [ 2 { 3 path: '', 4 component: FlightBookingComponent, 5 canActivate: [() => inject(AuthService).isAuthenticated()], 6 providers: [ 7 provideState(bookingFeature), 8 provideEffects(BookingEffects) 9 ], 10 children: [ 11 [...] 12 ], 13 }, 14 ];



Please note that while we call provideStore in the main.ts to set up the store, we need to call provideState (!) in further parts of the application to set up additional feature slices for them. However, provideEffects can be called in both places to set up effects for the root level but also feature slices.

After this modification, the application is migrated to Standalone Components and APIs. Run it via

1 ng serve -o





Conclusion


The new schematics automate the migration to Standalone Components. In three steps, the whole application or just a part of it is moved over to the new lightweight way of working with Angular. After each step, we can check the performed modification and intervene.





Signals in Angular: The Future of Change Detection


Sarah Drasner, Director of Engineering at Google, spoke of an Angular renaissance on Twitter. That’s pretty much it because several innovations have made Angular extremely attractive in the last few releases. Probably the most important are standalone components and standalone APIs.

Next, the Angular team takes care of renewing the change detection. It should be more lightweight and powerful. To do this, Angular will rely on a reactive mechanism called Signals, which several other frameworks have already adopted.

Signals will be available from Angular 16. Similar to standalone components, they initially come as a developer preview so that early adopters can gain initial experience.

In this chapter, I will go into this new mechanism and show how it can be used in an Angular application.

📂 Source Code(see branches signals and signal-rxjs-interop)





Change Detection Today: Zone.js


Angular currently assumes that any event handler can theoretically change any bound data. For this reason, after the execution of event handlers, the framework checks all bindings in all components for changes by default. In the more powerful OnPush mode, which relies on Immutables and Observables, Angular can drastically reduce the number of checked components.

Regardless if we go with the default behavior or OnPush, Angular needs to know when event handlers have run. This is challenging because the browser, not the framework, triggers the event handlers. Zone.js helps here: Using monkey patching, it extends JavaScript objects such as window or document and prototypes such as HtmlButtonElement, HtmlInputElement or Promise. By modifying such standard constructs, Zone.js can determines when an event handler has run. It then notifies Angular that it takes care of the change detection:

Change

While this approach has worked well in the past, it still comes with a few downsides:

Zone.js monkey patching is magic. Browser objects are modified, and errors are difficult to diagnose.

Zone.js has an overhead of around 100 KB. While negligible for larger applications, this is a deal-breaker when deploying lightweight web components.

Zone.js cannot monkey-patch async and await as they are keywords. Therefore, the CLI still converts these statements into promises, even though all supported browsers already support async and await natively.

When changes are made, entire components including their predecessors, are always checked in the component tree. It is currently not possible to directly identify changed components or to just update the changed parts of a component.



Exactly these disadvantages are now compensated with Signals.





Change Detection Tomorrow: Signals


A signal is a simple reactive building block: it holds a value consumers can read. Depending on the nature of the signal, the value can also be changed, after which the signal notifies all consumers:

How

If the consumer is a template, it can notify Angular about changed bindings. In the terminology of the Angular team, the signal occurs as a so-called producer. As described below, there are also other building blocks that can fill this role.





Using Signals


For using Signals with data binding, properties to be bound are expressed as signals:

1 @Component([…]) 2 export class FlightSearchComponent { 3 4 private flightService = inject(FlightService); 5 6 from = signal('Hamburg'); 7 to = signal('Graz'); 8 flights = signal<Flight[]>([]); 9 10 […] 11 12 }



It should be noted here that a signal always has a value by definition. Therefore, a default value must be passed to the signal function. If the data type cannot be derived from this, the example specifies it explicitly via a type parameter.

The Signal’s getter is used to read the value of a signal. Technically, this means that the signal is called like a function:

1 async search(): Promise<void> { 2 if (!this.from() || !this.to()) { 3 return; 4 } 5 const flights = await this.flightService.findAsPromise(this.from(), this.to()); 6 this.flights.set(flights); 7 }



To set the value, the signal offers an explicit setter in the form of a set method. The example shown uses the setter to stow the loaded flights. The getter is also used for data binding in the template:

1 <div *ngIf="flights().length > 0"> 2 {{flights().length}} flights found! 3 </div> 4 5 <div class="row"> 6 <div *ngFor="let f of flights()"> 7 <flight-card [item]="f" /> 8 </div> 9 </div>



Method calls were frowned upon in templates in the past, especially since they could lead to performance bottlenecks. However, this does not generally apply to uncomplex routines such as getters. In addition, the template will appear here as a consumer, and as such, it can be notified of changes.

As of writing this, ngModel has not directly supported two-way data binding. However, it can be done by hand by setting up a property binding reading the signal and an event binding updating the signal with the field’s current value:

1 <form #form="ngForm"> 2 <div class="form-group"> 3 <label>From:</label> 4 <input [ngModel]="from()" (ngModelChange)="from.set($event)" name="from" class="\ 5 form-control"> 6 </div> 7 8 <div class="form-group"> 9 <label>To:</label> 10 <input [ngModel]="to()" (ngModelChange)="to.set($event)" name="to" class="form-c\ 11 ontrol"> 12 </div> 13 14 <div class="form-group"> 15 <button class="btn btn-default" (click)="search()">Search</button> 16 <button class="btn btn-default" (click)="delay()">Delay</button> 17 </div> 18 </form>



In a future version, the Angular team will adapt the forms handling to Signals.





Updating Signals


In addition to the setter shown earlier, Signals also provide an update method for projecting the current value to a new one:

1 this.flights.update(f => { 2 const flight = f[0]; 3 const date = addMinutes(flight.date, 15); 4 const updated = {...flight, date}; 5 6 return [ 7 updated, 8 ...f.slice(1) 9 ]; 10 });





Signals Need to be Immutable


By default, a Signal content has to be immutable. For this reason, just updating the flight date in the previous section would not be sufficient. Instead, we have to clone the changed parts, so that they get a new object reference.

By comparing these references, Angular’s OnPush change detection strategy can efficiently determine the changed parts of an object managed by a Signal. In the previous section, the Array and the first flight get an new object reference. The other flights are not changed and hence just copied over using slice. As a result, they keep their object reference.





Calculated Values, Side Effects, and Assertions


Some values are derived from existing values. Angular provides calculated signals for this:

1 flightRoute = computed(() => this.from() + ' to ' + this.to());



Such a signal is read-only and appears as both a consumer and a producer. As a consumer, it retrieves the values of the signals used - here from and to - and is informed about changes. As a producer, it returns a calculated value.

If you want to consume signals programmatically, you can use the effect function:

1 constructor() { 2 effect(() => { 3 console.log('from:', this.from()); 4 console.log('route:', this.flightRoute()); 5 }); 6 }



The effect function executes the transferred lambda expression and registers itself as a consumer for the signals used. When one of these signals changes, the effect is triggered again.

If a signal changes several times in a row, or if several signals change one after the other, undesired interim results could occur. Let’s imagine we change the search filter Hamburg - Graz to London - Paris:

1 setTimeout(() => { 2 this.from.set('London'); 3 this.to.set('Paris'); 4 }, 2000);



Here, London - Graz could come immediately after the setting from to London. Like many other Signal implementations, Angular’s implementation prevents such occurrences. The Angular team’s readme, which also explains the push/pull algorithm used, calls this desirable assurance “glitch-free”.





Effects Need an Injection Context


An essential limitation of Effects is that they can only be used in an injection context. This is everywhere inject is allowed: in the constructor, as default values of class fields, and in provider factories. Also, you can use the runInInjectionContext function to run code in an injection context.

Hence, the effect set up in the constructor above would fail when placed in ngOnInit or in another method:

1 ngOnInit(): void { 2 // Effects are not allowed here. 3 // Hence, this will fail: 4 effect(() => { 5 console.log('route:', this.flightRoute()); 6 }); 7 }



In this case, you’d get the following error:

ERROR Error: NG0203: effect() can only be used within an injection context such as a constructor, a factory function,



The technical reason is that effects use inject to get hold of the current DestroyRef. This service provided since Angular 16 helps to find out about the life span of the current building block, e. g. the current component or service. The effect uses the DestroyRef to “unsubscribe” itself when this building block is about to be destroyed.

For this reason, you would typically setup your effects in the constructor as shown in the last section. If you really want to setup an effect somewhere else, you can go with the runInInjectionContext function. However, it needs a reference to an Injector:

1 injector = inject(Injector); 2 3 ngOnInit(): void { 4 runInInjectionContext(this.injector, () => { 5 effect(() => { 6 console.log('route:', this.flightRoute()); 7 }); 8 }); 9 }





Writing Signals in Effects


If an effect wrote a Signal, a cycle might be created. Just imagine an effect that reads the Signal a and updates Signal b and another effect doing the same the other way round. To prevent such situations, effects are not allowed to write Signals. For instance, let’s assume an effect synchronizing our from and to Signals:

1 effect(() => { 2 // Writing into signals is not allowed here: 3 this.to.set(this.from()); 4 });



This effect would lead to the following error:

ERROR Error: NG0600: Writing to signals is not allowed in a computed or an effect by default. Use allowSignalWrites in the CreateEffectOptions to enable this inside effects.



As an alternative, you should consider the usage of computed. If this does not work or if you really want to write into a signal, setting the allowSignalWrites property to true deactivates this child safety facility:

1 effect(() => { 2 this.to.set(this.from()); 3 }, { allowSignalWrites: true })





Signals and Change Detection


Similar to an Observable bound to a template using the async-Pipe, a bound Signal triggers change detection. This also works for the more efficient OnPush mode:

1 @Component({ 2 [...] 3 changeDetection: ChangeDetectionStrategy.OnPush 4 }) 5 export class FlightSearchComponent { [...] } 6 7 [...] 8 9 @Component({ 10 [...] 11 changeDetection: ChangeDetectionStrategy.OnPush 12 }) 13 export class FlightCardComponent { [...] }



However, to help Angular in OnPush mode to also find out about child components to look at, you have to use Immutables, as discussed above.





RxJS Interop


Admittedly, at first glance, signals are very similar to a mechanism that Angular has been using for a long time, namely RxJS Observables. However, signals are deliberately kept simpler.

If you need the power of RxJS and its operators, you can however convert them to Observables. The namespace @angular/core/rxjs-interop contains a function toObservable converting a Signal into an Observable and a function toSignal for the other way around. They allow using the simplicity of signals with the power of RxJS.

The following listing illustrates the use of these two methods by expanding the example shown into a debounced type-ahead:

1 @Component([...]) 2 export class FlightSearchComponent { 3 private flightService = inject(FlightService); 4 5 from = signal('Hamburg'); 6 to = signal('Graz'); 7 basket = signal<Record<number, boolean>>({ 1: true }); 8 flightRoute = computed(() => this.from() + ' to ' + this.to()); 9 10 from$ = toObservable(this.from); 11 to$ = toObservable(this.to); 12 13 flights$ = combineLatest({ from: this.from$, to: this.to$ }).pipe( 14 filter(c => c.from.length >= 3 && c.to.length >= 3), 15 debounceTime(300), 16 switchMap(c => this.flightService.find(c.from, c.to)) 17 ); 18 19 flights = toSignal(this.flights$, { 20 initialValue: [] 21 }); 22 }



The example converts the signals from and to into the observables from$ and to$ and combines them with combineLatest. As soon as one of the values changes, filtering and debouncing occur before switchMap triggers the backend request. One of the benefits flattening operators like switchMap come with are guarantees in terms of asynchronicity. These guarantees help to avoid race conditions.

The initialValue passed to toSignal is needed because Signals always have a value. On the contrary, Observables might not emit a value at all. If you are sure your Observable has an initial value, e.g., because it’s a BehaviorSubject or because of using the startsWith operator, you can also set requireSync to true:

1 flights = toSignal(this.flights, { 2 requireSync: true 3 });



If you neither set initialValue nor requireSync, the type of the returned Signal also supports the undefinied type, allowing an initial value of undefined. In the example shown, this would result in a Signal<Flight[] | undefinied> instead of Signal<Flight[]>. Consequently, your code has to check for undefined too.





NGRX and Other Stores?


So far, we directly created and managed the Signals. However, stores like NGRX will provide some additional convenience. For instance, beginning with NGRX 16, the store will come with a selectSignal method:

1 store = inject(Store); 2 flights = this.store.selectSignal(selectFlights);



Other store implementations will have similar adoptions.





Conclusion


Signals make Angular lighter and point the way to a future without Zone.js. They enable Angular to find out about components that need to be updated directly.

The Angular team remains true to itself: Signals are not hidden in the substructure or behind proxies but made explicit. Developers therefore always know which data structure they are actually dealing with. Also, signals are just an option. No one needs to change legacy code and a combination of traditional change detection and signal-based change detection will be possible.

In general, it should be noted that Signals is still in an early phase and will ship with Angular 16 as a developer preview. This allows early adopters to try out the concept and provide feedback. With this, too, the Angular team proves that the stability of the ecosystem is important to them - an important reason why many large enterprise projects rely on the framework penned by Google.





Component Communication with Signals: Inputs, Two-Way Bindings, and Content/ View Queries


Signals will shape Angular’s future. However, the Signals concept itself is just one part of the overall story. We also need a way to communicate with (sub)components via Signals. Angular 17.1 brought Input Signals, and with Angular 17.2 we’ve got Signal-based Two-way Bindings and support for content and view queries. To align with Input Signals, Version 17.3 provides a new output API.

In this chapter, I show how to use these new possibilities.

📁 Source Code (see different branches)





Input Signals


Inputs Signals allow us to receive data via Property Bindings in the form of Signals. For describing the usage of Signal Inputs, I’m using a simple OptionComponent representing a – for the sake of simplicity – non-selectable option. Here, three of them are presented:

Three simple option components





Defining an Input Signal


Input Signals are the counterpart to the traditional @Input decorator:

1 @Component({ 2 selector: 'app-option', 3 standalone: true, 4 imports: [], 5 template: ` 6 <div class="option"> 7 {{ label() }} 8 </div> 9 `, 10 styles: [...] 11 }) 12 export class OptionComponent { 13 label = input.required<string>(); 14 }



This input function is picked up by the Angular Compiler, emitting source code for property bindings. Hence, we should only use it together with properties. The other communication concepts discussed here also use this technique.

Having a function instead of a decorator allows to inform TypeScript about the proper type and whether it includes undefined. In the example shown before, label becomes an InputSignal<string> – an Input Signal providing a string. An undefined value is not possible as input.required defines a mandatory property.

An InputSignal is always read-only and can be used like a Signal. The template above, for instance, requests its current value by calling the getter (label()).





Binding to an Input Signal


In the case of our InputSignal<string>, the caller has to pass a string:

1 <app-option label="Option #1"> 2 <app-option [label]="myStringProperty">



If this string comes from a Signal, we have to read it in the template:

1 <app-option [label]="mySignalProperty()">





Computed Signals and Effects as a Replacement for Life Cycle Hooks


All changes to the passed Signal will be reflected by the InputSignal in the component. Internally, both Signals are connected via the graph Angular is maintaining. Life cycle hooks like ngOnInit and ngOnChanges can now be replaced with computed and effect:

1 markDownTitle = computed(() => '# ' + this.label()) 2 3 constructor() { 4 effect(() => { 5 console.log('label updated', this.label()); 6 console.log('markdown', this.markDownTitle()); 7 }); 8 }





Options for Input Signals


Here are some further options for setting up an InputSignal:

Source Code Description

label = input<string>(); Optional property represented by an InputSignal<string | undefined>

label = input(‘Hello’); Optional property represented by an InputSignal<string> with an initial value of Hello

label = input<string | undefined>(‘Hello’); Optional property represented by an InputSignal<string\| undefined> with an initial value of Hello





Required Inputs Cannot Have a Default Value!


By definition, input.required cannot have a default value. This makes sense at first glance, however, there is a pitfall: If you try to read the value of a required input before it’s been bound, Angular throws an exception.

Hence, you cannot directly access it in the constructor. Instead, you can use ngOnInit or ngOnChanges. Also, using inputs within computed or effect is always safe, as they are only first triggered when the component has been initialized:

1 @Component([...]) 2 export class OptionComponent implements OnInit, OnChanges { 3 label = input.required<string>(); 4 5 // safe 6 markDownTitle = computed(() => '# ' + this.label()) 7 8 constructor() { 9 	// this would cause an exception, 10 	// as data hasn't been bound so far 11 	console.log('label', this.label); 12 13 	effect(() => { 14 		// safe 15 		console.log('label', this.label); 16 	}) 17 } 18 19 ngOnInit() { 20 	// safe 21 	console.log('label', this.label); 22 } 23 24 ngOnChanges() { 25 	// safe 26 	console.log('label', this.label); 27 } 28 }





Aliases for Input Signals


Both input and input.require also take a parameter object that allows the definition of an alias:

1 label = input.required({ alias: "title" });



In this case, the caller needs to bind to the property name defined by the alias:

1 <app-option title="Option #1"> 2 <app-option [title]="myStringProperty"> 3 <app-option [title]="mySignalProperty()"></app-option></app-option 4 ></app-option>



In most cases, you should prevent the usage of aliases, as they create an unnecessary indirection. An often-seen exception to this rule is renaming one of a Directive’s properties to match the configured attribute selector.





Transformer for Input Signals


Transformer have already been available for traditional @Inputs. They allow the transformation of a value passed via a property binding. In the following case, the transformer booleanAttribute that can be found in angular/core is used:

1 @Component({ 2 selector: 'app-option', 3 standalone: true, 4 imports: [], 5 template: ` 6 <div class="option"> 7 {{ label() }} @if (featured()) { ⭐ } 8 </div> 9 `, 10 styles: [...] 11 }) 12 export class OptionComponent { 13 label = input.required<string>(); 14 featured = input.required({ 15 transform: booleanAttribute 16 }) 17 }



This transformer converts strings to booleans:

1 <app-option label="Option #1" featured="true"></app-option>



Also, if the attribute is present but no value was assigned, true is assumed:

1 <app-option label="Option #1" featured></app-option>



This Signal’s type is InputSignal<boolean, unknown>. The first type parameter (boolean) represents the value received from the transformer; the second one (unknown) is the value bound in the caller’s template and passed to the transformer. Besides booleanAttribute, @angular/core also provides a numberAttribute transformer that converts passed strings to numbers.

If you want to implement a custom transformer, just provide a function taking the bound value and returning the value that should be used by the called component:

1 function boolTranformer(value: unknown): boolean { 2 return value !== "no"; 3 }



Then, register this function in your input:

1 @Component([...]) 2 export class OptionComponent { 3 label = input.required<string>(); 4 featured = input.required({ 5 transform: boolTranformer 6 }) 7 }





Two-Way Data Binding with Model Signals


Input Signals are read-only. If you want to pass a Signal that can be updated by the called component, you need to set up a so-called Model Signal. To demonstrate this, I’m using a simple TabbedPaneComponent:

TabbedPane

This is how the consumer can use this component:

1 <app-tabbed-pane [(current)]="current"> 2 <app-tab title="1st tab"> Lorem, ipsum dolor sit amet ... </app-tab> 3 <app-tab title="2nd tab"> Sammas ergo gemma, ipsum dolor ... </app-tab> 4 <app-tab title="3nd tab"> Gemma ham ipsum dolor sit ... </app-tab> 5 </app-tabbed-pane> 6 7 <p class="current-info">Current: {{ current() }}</p>



It gets several TabComponents passed. Also, a Signal current is bound via a Two-way Binding. For allowing this, the TabbedPaneComponent needs to provide a Model Signal using model:

1 @Component([...]) 2 export class TabbedPaneComponent { 3 current = model(0); 4 [...] 5 }



Here, 0 is the initial value. The options are similar to the ones for input: model.required defines a mandatory property, and you can provide an alias via an options object. However, a transformer can not be defined.

If this component updates the Model Signal, the new value is propagated up to the the Signal bound in the template:

1 current.set(1);





Two-Way Data Binding as Combination of Input and Output


As usual in Angular, also Signal-based Two-way Bindings can be defined with a (read-only) Input and a respective Output. The Output’s name must be the Input’s name with the suffix Change. Hence, for current we need to define currentChange:

1 @Component([...]) 2 export class TabbedPaneComponent { 3 current = input(0); 4 currentChange = output<number>(); 5 }



For setting up an Output, we use the new output API. For triggering an event, the application has to call the output’s emit method:

1 <button [...] (click)="currentChange.emit($index)">{{tab.title()}}</button>





Content Queries with Signals


The TabbedPaneComponent introduced in the previous section also allows us to showcase another option: Content Queries that get hold of projected Components or Directives.

As shown above, a TabbedPaneComponent gets several TabComponents passed. They are projected into the TabbedPaneComponent’s view. However, we only want to display one of them at a given time. Hence, the TabbedPaneComponent needs to get programmatic access to its TabComponents. This can be done with the new contentChildren function:

1 @Component({ 2 selector: 'app-tabbed-pane', 3 standalone: true, 4 imports: [], 5 template: ` 6 <div class="pane"> 7 <div class="nav" role="group"> 8 @for(tab of tabs(); track tab) { 9 <button 10 [class.secondary]="tab !== currentTab()" 11 (click)="activate($index)"> 12 {{tab.title()}} 13 </button> 14 } 15 </div> 16 <article> 17 <ng-content></ng-content> 18 </article> 19 </div> 20 `, 21 styles: [...] 22 }) 23 export class TabbedPaneComponent { 24 current = model(0); 25 tabs = contentChildren(TabComponent); 26 currentTab = computed(() => this.tabs()[this.current()]); 27 28 activate(active: number): void { 29 this.current.set(active); 30 } 31 }



The function contentChildren is the counterpart to the traditional @ContentChildren decorator. As TabComponent was passed as a so-called locator, it returns a Signal with an Array holding all projected TabComponents.

Having the projected nodes as a Signal allows us to project them using computed reactively. The shown example uses this option to derive a Signal currentTab.

The projected TabComponent uses this Signal to find out whether it should be visible:

1 @Component({ 2 selector: "app-tab", 3 standalone: true, 4 imports: [], 5 template: ` 6 @if(visible()) { 7 <div class="tab"> 8 <h2>{{ title() }}</h2> 9 <ng-content></ng-content> 10 </div> 11 } 12 `, 13 }) 14 export class TabComponent { 15 pane = inject(TabbedPaneComponent); 16 17 title = input.required<string>(); 18 19 visible = computed(() => this.pane.currentTab() === this); 20 }



For this, we need to know that we can get all parents located in the DOM via dependency injection. The visible Signal is derived from the currentTab Signal.

This procedure is usual in the reactive world: Instead of imperatively setting values, they are declaratively derived from other values.





Content Queries for Descendants


By default, a Content Query only unveils direct content children. “Grandchildren”, like the 3rd tab below, are ignored:

1 <app-tabbed-pane [(current)]="current"> 2 <app-tab title="1st tab"> Lorem, ipsum dolor sit amet ... </app-tab> 3 <app-tab title="2nd tab"> Sammas ergo gemma, ipsum dolor ... </app-tab> 4 5 <div class="danger-zone"> 6 <app-tab title="3nd tab"> 7 Here, you can delete the whole internet! 8 </app-tab> 9 </div> 10 </app-tabbed-pane>



To also get hold of such nodes, we can set the option descendants to true:

1 tabs = contentChildren(TabComponent, { descendants: true });





Output API


For the sake of API symmetricity, Angular 17.3 introduced a new output API. As already shown before, an output function is now used for defining an event provided by a component. Similar to the new input API, the Angular Compiler picks up the call to the output and emits respective code. The returned OutputEmitterRef’s emit method is used to trigger the event:

1 @Component([...]) 2 export class TabbedPaneComponent { 3 current = model(0); 4 tabs = contentChildren(TabComponent); 5 currentTab = computed(() => this.tabs()[this.current()]); 6 7 tabActivated = output<TabActivatedEvent>(); 8 9 activate(active: number): void { 10 const previous = this.current(); 11 this.current.set(active); 12 this.tabActivated.emit({ previous, active }); 13 } 14 }





Providing Observables as Outputs


Besides this simple way of setting up outputs, you can use an Observable as the source for an output. For this, you find a function outputFromObservable in the RxJS interop layer:

1 import { 2 outputFromObservable, 3 toObservable 4 } from '@angular/core/rxjs-interop'; 5 [...] 6 7 @Component([...]) 8 export class TabbedPaneComponent { 9 current = model(0); 10 tabs = contentChildren(TabComponent); 11 currentTab = computed(() => this.tabs()[this.current()]); 12 13 tabChanged$ = toObservable(this.current).pipe( 14 scan( 15 (acc, active) => ({ active, previous: acc.active }), 16 { active: -1, previous: -1 } 17 ), 18 skip(1), 19 ); 20 21 tabChanged = outputFromObservable(this.tabChanged$); 22 23 activate(active: number): void { 24 this.current.set(active); 25 } 26 27 }



The function outputFromObservable converts an Observable to an OutputEmitterRef. In the shown example, the scan operator remembers the previous activated tab and skip ensures that no event is emitted when initially setting current. The latter one provides feature parity with the before-shown example.





View Queries with Signals


While a Content Query returns projected nodes, a View Query returns nodes from its own view. These are nodes found in the template of the respective component. In most cases, using data binding instead is the preferable solution. However, getting programmatic access to a view child is needed in some situations.

To demonstrate how to query view children, I use a simple form for setting a username and a password:

Form for setting username and password

Both input fields are marked as required. If the validation fails when pressing Save, the first field with a validation error should get the focus. For this, we need access to the NgForm directive the FormModule adds to our form tag as well as to the DOM nodes representing the input fields:

1 @Component({ 2 selector: "app-form", 3 standalone: true, 4 imports: [FormsModule, JsonPipe], 5 template: ` 6 <h1>Form Demo</h1> 7 <form autocomplete="off"> 8 <input 9 [(ngModel)]="userName" 10 placeholder="User Name" 11 name="userName" 12 #userNameCtrl 13 required 14 /> 15 <input 16 [(ngModel)]="password" 17 placeholder="Password" 18 type="password" 19 name="password" 20 #passwordCtrl 21 required 22 /> 23 <button (click)="save()">Save</button> 24 </form> 25 `, 26 styles: ` 27 form { 28 max-width: 600px; 29 } 30 `, 31 }) 32 export class FormDemoComponent { 33 form = viewChild.required(NgForm); 34 35 userNameCtrl = 36 viewChild.required<ElementRef<HTMLInputElement>>("userNameCtrl"); 37 passwordCtrl = 38 viewChild.required<ElementRef<HTMLInputElement>>("passwordCtrl"); 39 40 userName = signal(""); 41 password = signal(""); 42 43 save(): void { 44 const form = this.form(); 45 46 if (form.controls["userName"].invalid) { 47 this.userNameCtrl().nativeElement.focus(); 48 return; 49 } 50 51 if (form.controls["password"].invalid) { 52 this.passwordCtrl().nativeElement.focus(); 53 return; 54 } 55 56 console.log("save", this.userName(), this.password()); 57 } 58 }



Both are done using the viewChild function. In the first case, the example passes the type NgForm as the locator. However, just locating the fields with a type does not work, as there might be several children with this type. For this reason, the inputs are marked with handles (#userName and #password), and the respective handle’s name is passed the locator.

View children can be represented by different types: The type of the respective Component or Directive, an ElementRef representing its DOM node, or a ViewContainerRef. The latter one is used in the next section.

The desired type can be mentioned using the read option used in the previous example.





Queries and ViewContainerRef


There are situations where you need to dynamically add a component to a placeholder. Examples are modal dialogs or toasts. An easy way to achieve this is using the *ngComponentOutlet directive. A more flexible way is querying the ViewContainerRef of the placeholder.

You can think about a View Container as an invisible container around each Component and piece of static HTML. After getting hold of it, you can add further Components or Templates.

To demonstrate this, I’m using a simple example showing a toast:

Example displaying a toast

The example uses an ng-container as a placeholder:

1 @Component({ 2 selector: 'app-dynamic', 3 standalone: true, 4 imports: [], 5 template: ` 6 <h2>Toast Demo</h2> 7 <button (click)="show()">Show Toast</button> 8 <ng-container #placeholder></ng-container> 9 `, 10 styles: [...] 11 }) 12 export class ToastDemoComponent { 13 counter = 0; 14 placeholder = viewChild.required('placeholder', { read: ViewContainerRef }); 15 16 show() { 17 const ref = this.placeholder()?.createComponent(ToastComponent); 18 this.counter++; 19 ref?.setInput('label', 'Message #' + this.counter); 20 setTimeout(() => ref?.destroy(), 2000); 21 } 22 23 }



The read property makes clear that we don’t want to read the placeholder component but its ViewContainerRef. The createComponent method instantiates and adds a ToastComponent. The returned ComponentRef is used to set the new component’s label property. For this, its setInput method is used. After two seconds, the destroy method removes the toast again.

For the sake of simplicity, the component ToastComponent was hard-coded here. In more generic real-world scenarios, the component to use can be configured, e.g., by calling a service method, taking the Component type, and notifying another Component that adds a Component of this type to a placeholder.





Programmatically Setting up an Output


The previous example called setInput for assigning a value to the ToastComponent’s title input. Here, I want to discuss how to define event handlers for such dynamically added components.

Let’s assume, the ToastComponent shows a confirmation link:

Toast with confirm link

When clicking this link, it emits an confirmed event:

1 @Component([...]) 2 export class ToastComponent { 3 label = input.required<string>(); 4 confirmed = output<string>(); 5 6 confirm(): void { 7 this.confirmed.emit(this.label()); 8 } 9 }



To set up an handler for this event, we can directly use the returned ComponentRef’s instance property. It points to the added component instance and hence provides access to all its properties:

1 @Component([...]) 2 export class ToastDemoComponent { 3 counter = 0; 4 placeholder = viewChild.required('placeholder', { read: ViewContainerRef }); 5 6 show() { 7 const ref = this.placeholder()?.createComponent(ToastComponent); 8 this.counter++; 9 const title = 'Message #' + this.counter; 10 ref.setInput('label', title); 11 12 // Event handler for confirm output 13 ref.instance.confirmed.subscribe(title => { 14 ref?.destroy(); 15 console.log('confirmed', title); 16 }); 17 18 setTimeout(() => ref?.destroy(), 5000); 19 } 20 21 }



The OutputEmitterRef’s subscribe method allows to define an event handler. In our case, it just removes the toast using destroy and logs the received string to the console.

However, this example comes with a small beauty mistake. Regardless whether the user clicks the confirmation link or not, the example calls destroy after 5 seconds. Hence, the toast might be removed twice: Once after the confirmation and another time after displaying it for 5 seconds.

Fortunately, destroying a component twice does not result in an error. To solve this, we could introduce a destroyed flag . The next section shows a more powerful approach: Consuming outputs as Observables.





Consuming Outputs as Observables


Even though the OutputEmitterRef provides a subscribe method, it is not an Observable. However, the original EventEmitter used together with the @Output decorator was. To get back all the possibilities associated with Observable-based outputs, you can use the function outputToObservable that is part of the RxJS interop layer:

1 import { outputToObservable } from '@angular/core/rxjs-interop'; 2 [...] 3 4 @Component([...]) 5 export class ToastDemoComponent { 6 counter = 0; 7 placeholder = viewChild.required('placeholder', { read: ViewContainerRef }); 8 9 show() { 10 const ref = this.placeholder()?.createComponent(ToastComponent); 11 this.counter++; 12 const title = 'Message #' + this.counter; 13 ref.setInput('label', title); 14 15 const confirmed$ = outputToObservable(ref.instance.confirmed) 16 .pipe(map(title => ({ trigger: 'confirmed', title }))); 17 18 const timer$ = timer(5000); 19 .pipe(map(() => ({ trigger: 'timeout', title }))); 20 21 race(confirmed$, timer$).subscribe(action => { 22 ref?.destroy(); 23 console.log('action', action); 24 }); 25 26 } 27 28 }



The function outputToObservable converts an OutputEmitterRef to an Observable. The shown example uses it to express both, the confirm event and the 5 sec timeout as observable. The race operator ensures that only the Observable that first issues a value is used.

The Observable returned by outputToObservable completes when Angular destroys the the output’s component. For this reason, there is no need to unsubscribe by hand.





Feature Parity between Content and View Queries


So far, we have worked with contentChildren to query several projected children and viewChild to get hold of one node in the view. However, both concepts have feature parity: For instance, there is also a contentChild and a viewChildren function.

Also, all the options we’ve used above for View or Content Queries, like using handles as locators or using the read property, work for both kinds of queries.





Conclusion


Several new functions replace property decorators and help to set up data binding concepts. These functions are picked up by the Angular compiler emitting respective code.

The function input defines Inputs for property bindings, model defines Inputs for Two Way Data Binding, and contentChild(ren) and viewChild(ren) take care of Content and View Queries. Using these functions results in Signals that can be projected with computed and used within effects.





Successful with Signals in Angular - 3 Effective Rules for Your Architecture


It is undisputed that Signals will shape the future of Angular. At first glance, they seem very easy to use: The setters take new values, the getters deliver the current values and the templates as well as Effects are notified about changes.

Now you might be tempted to treat Signals like normal variables. This works in principle and can be a practical option when migrating existing code. However, in this case, the advantages of Signals and reactive systems only result to a limited extent. There are also some pitfalls and the code is not as straightforward and hence not as maintainable as it could be.

In order to prevent such situations, I would like to use this chapter to give you three simple rules of thumb that allow Signals to be used as idiomatically as possible.

📁 Source Code (see different branches!)

Big thanks to Angular’s <PRESIDIO_ANONYMIZED_PERSON> for a great discussion that led to the idea for this chapter and for providing feedback.





Initial Example With Some Room for Improvement


I would like to discuss the three promised rules using a simple Angular application:

Example application

The first implementation considered is not reactive and also offers some room for improvement:

1 @Component([…]) 2 export class DessertsComponent implements OnInit { 3 #dessertService = inject(DessertService); 4 #ratingService = inject(RatingService); 5 […] 6 7 originalName = ''; 8 englishName = ''; 9 loading = false; 10 11 desserts: Dessert[] = []; 12 13 ngOnInit(): void { 14 this.search(); 15 } 16 17 search(): void { 18 const filter: DessertFilter = { 19 originalName: this.originalName, 20 englishName: this.englishName, 21 }; 22 23 this.loading = true; 24 25 this.#dessertService.find(filter).subscribe({ 26 next: (desserts) => { 27 this.desserts = desserts; 28 this.loading = false; 29 }, 30 error: (error) => { […] }, 31 }); 32 } 33 34 toRated(desserts: Dessert[], ratings: DessertIdToRatingMap): Dessert[] { 35 return desserts.map((d) => 36 ratings[d.id] ? { ...d, rating: ratings[d.id] } : d, 37 ); 38 } 39 40 loadRatings(): void { 41 this.loading = true; 42 43 this.#ratingService.loadExpertRatings().subscribe({ 44 next: (ratings) => { 45 const rated = this.toRated(this.desserts, ratings); 46 this.desserts = rated; 47 this.loading = false; 48 }, 49 error: (error) => { […] }, 50 }); 51 } 52 […] 53 }



Since the properties to be bound are neither Observables nor Signals, the strategy OnPush cannot be used for improving the data binding performance. Upon closer inspection, we also notice that the loadRatings method updates the desserts array, even though its actual task – loading ratings – has nothing to do with it.

Additionally, developers must remember that after any changes to the ratings, the desserts array must also be modified. This is exactly what can lead to hard-to-maintain code and hidden bugs – especially when both desserts and ratings change at different points. Things become even more complex when additional data structures have to be taken into account in these calculations. The first rule of thumb presented here solves this issue.





Rule 1: Derive State Synchronously Wherever Possible


The previously mentioned disadvantages can be compensated for with Signals. Since the introduction of Signals makes the component reactive, OnPush can now be activated. In addition, the component can derive its state from the individual Signals using computed synchronously:

1 @Component({ 2 […], 3 changeDetection: ChangeDetectionStrategy.OnPush, 4 }) 5 export class DessertsComponent implements OnInit { 6 #dessertService = inject(DessertService); 7 #ratingService = inject(RatingService); 8 9 originalName = signal(''); 10 englishName = signal(''); 11 loading = signal(false); 12 13 desserts = signal<Dessert[]>([]); 14 ratings = signal<DessertIdToRatingMap>({}); 15 ratedDesserts = computed(() => this.toRated(this.desserts(), this.ratings())); 16 17 […] 18 19 loadRatings(): void { 20 this.loading.set(true); 21 22 this.#ratingService.loadExpertRatings().subscribe({ 23 next: (ratings) => { 24 this.ratings.set(ratings); 25 this.loading.set(false); 26 }, 27 error: (error) => { […] } 28 }); 29 30 […] 31 }



This makes the code a lot more straightforward: The loadRatings method simply loads the ratings and places them in a signal. The computed Signal ratedDesserts takes care of mergining desserts and ratings. No matter when and where the application updates desserts or ratings, ratedDesserts is always up to date.

Primary usage scenario of Signals: binding values reactively to the view.



When applying this pattern, it is important to note that computed can currently only derive state in a synchronous manner. This has to do with the primary usage scenario of Signals: binding values reactively to the view. It’s possible that the Angular team will extend Signals to asynchronous scenarios over time. However, at the moment it is necessary to resort to other means. Rule 3 offers a straightforward approach to this.





Rule 2: Avoid Effects for Propagating State


Effects are the right choice when presenting values cannot be achieved via data binding. However, they bring some pitfalls when used for propagating change. In the next sections, I elaborate on this.





Proper Usage of Effects


In most cases, Signals are bound in the template. However, it happens that the desired form of output cannot be achieved via data binding. An example of this is outputting a Signal to the console for debugging purposes. Another example are toasts that can be activated via services and are intended to present the value of a Signal. For these cases, Angular provides Effects:

1 […] 2 constructor() { 3 effect(() => { 4 console.log('originalName', this.originalName()); 5 console.log('englishName', this.englishName()); 6 }); 7 8 effect(() => { 9 this.#toastService.show(this.desserts().length + ' desserts loaded!'); 10 }); 11 } 12 […]





Signals are Glitch-free


When writing code like in the previous section, we need to be aware that Signals are glitch-free. That means that if you change a signal several times in a row (within a stack frame), only the last change will be seen by the consumer, e.g. the effect:

1 @Component([...]) 2 export class AboutComponent { 3 4 constructor() { 5 const signal1 = signal('A'); 6 const signal2 = signal('B'); 7 8 effect(() => { 9 console.log('signal1', signal1()); 10 console.log('signal2', signal2()); 11 }); 12 13 signal1.set('C'); 14 signal1.set('D'); 15 16 signal1.set('E'); 17 18 signal2.set('F'); 19 } 20 }



In this case, we will only see the values E and F on the console. Indemediate values are skipped.

This shows that Signals are not intended for modelling events but for data we want to bind to the view. In the latter case, we just need the current value while binding intermediate values would be counterproductive. For this reasons, the effects shown in the previous section are only triggered once even if there are several changes in a row.

In cases where we want to express events, Observables are the way to go, as they don’t have this glitch-free guarantee by design.





Problematic Use of Effects


Even when using Effects, Signals are primarily used to transport the desired data into the view. In theory, however, Effects could also be used to transmit state to other Signals:

1 effect(() => { 2 this.originalName.set(this.englishName()); 3 });



However, such approaches have several disadvantages, which is why Angular prohibits writing Signals within Effects by default:

Error message when trying to set a Signal in an Effect

One of these disadvantages is that unmanageable change cascades and thus difficult-to-maintain code and cyclic dependencies can arise. Since Effects register implicitly with all Signals used, the associated problems may not even be noticeable at first glance. If you still want to use Effects for writing, you can make Angular let things slide by setting allowSignalWrites:

1 // Try hard to avoid this 2 effect(() => { 3 this.originalName.set(this.englishName()); 4 }, 5 { allowSignalWrites: true }, 6 );



Application code should only use allowSignalWrites as a last resort.



The general consensus in the community is that application code should only use allowSignalWrites as a last resort. On the other hand, libraries like NGRX use this option internally. In this case, however, the authors of the library are responsible for its correct use, so application developers don’t have to worry about it.

It is also important to note that the Effect itself registers with Signals in called methods too. For instance, the following Effect is triggered when Signals change within search:

1 // Try hard to avoid this 2 effect(() => { 3 this.search(); 4 }, 5 { allowSignalWrites: true }, 6 );



This leads to a further increase in complexity. At least this problem could be alleviated with built-in features:

1 // Try hard to avoid this 2 effect(() => { 3 const originalName = this.originalName(); 4 const englishName = this.englishName(); 5 untracked(() => { 6 this.load(originalName, englishName); 7 }) 8 } 9 );



The untracked function avoids the current reactive context spilling over to the called search method. Angular now also uses this pattern itself in selected cases. An example of this is triggering events in sub-components so that the event handler does not run in the reactive context of the code that triggered the event. Further popular libraries that use this technique are NGRX, NGRX Signal Store or ngextensions.





Strategies for Preventing Effects With Signal Writes


Effects that spread data via Signal writes can in many cases be prevented using the following approaches:

Consistent derivation of state using computed (see rule 1, above).

Direct use of events that caused the Signal to change.



Instead of calling search, as indicated above, in an Effect, the application could instead use the change event of the input fields for the search filters. Observables can also be used as a source for such actions. The search method could, for example, also be triggered by the valueChanges observable of a FormGroup. In cases where you have just Signals, they can be converted into Observables using the RxJS Interop offered by Angular:

1 @Component([…]) 2 export class DessertsComponent { 3 #dessertService = inject(DessertService); 4 #ratingService = inject(RatingService); 5 #toastService = inject(ToastService); 6 7 originalName = signal(''); 8 englishName = signal('Cake'); 9 loading = signal(false); 10 11 originalName$ = toObservable(this.originalName); 12 englishName$ = toObservable(this.englishName); 13 14 desserts$ = combineLatest({ 15 originalName: this.originalName$, 16 englishName: this.englishName$, 17 }).pipe( 18 filter((c) => c.originalName.length >= 3 || c.englishName.length >= 3), 19 debounceTime(300), 20 tap(() => this.loading.set(true)), 21 switchMap((c) => this.findDesserts(c)), 22 tap(() => this.loading.set(false)), 23 ); 24 25 desserts = toSignal(this.desserts$, { 26 initialValue: [], 27 }); 28 29 ratings = signal<DessertIdToRatingMap>({}); 30 ratedDesserts = computed(() => this.toRated(this.desserts(), this.ratings())); 31 32 findDesserts(c: DessertFilter): Observable<Dessert[]> { 33 return this.#dessertService.find(c).pipe( 34 catchError((error) => { 35 this.#toastService.show('Error loading desserts!'); 36 console.error(error); 37 return of([]); 38 }), 39 ); 40 } 41 […] 42 }



The flattening operators offered by RxJS provide guarantees for overlapping asynchronous actions and thus prevent race conditions.



The use of Observables has several advantages at this point: - In contrast to Signals, Observables are also suitable for triggering asynchronous actions. - toObservable function strips the current reactive context using untracked. - RxJS comes with a lot of powerful operators, like debounceTime. - The flattening operators offered by RxJS provide guarantees for overlapping asynchronous actions and thus prevent race conditions. In the example shown, switchMap ensures that when search queries overlap, only the result of the last one is used and all others are canceled.

In many cases, one could argue that instead of converting a Signal into an Observable, it would be more appropriate to directly use the event that led to the Signal change, as proposed above. On the other hand, as Angular APIs increasingly adopt Signal-based approaches, using them directly will likely become more convenient and feel more intuitive. Therefore, this appears to be a gray area where we need to be mindful of the consequences, such as those associated with the glitch-free guarantee of Signals.





Rule 3: Stores Simplify Reactive Data Flow


Stores like the classic NGRX Store or the lightweight NGRX Signal Store not only take care of state management, but also help to keep the reactive data flow manageable:

Unidirectional data flow with a store

The application forwards its intention to the store as part of an event. At this point, I use the term intention in an abstract, technology-neutral way, especially since different stores realize this aspect differently. With Redux and therefore also when using the classic NGRX store the application sends an action to the store, which forwards it to Reducer and Effects. For lightweight stores like the NGRX Signal Store, the application delegates to a method offered by the store instead.

Offloading asynchronous operations to the store also compensates for the fact that Signals are currently only designed for synchronous actions.



The store then takes action and initiates synchronous or asynchronous operations. If the application uses RxJS for this, race conditions can be avoided with the flattening operators, as mentioned above. Offloading asynchronous operations to the store also compensates for the fact that Signals are currently only designed for synchronous actions.

The result of these operations leads to a change in the state managed by the store. This state can be expressed by Signals, which can be mapped to other Signals using computing (see rule 1). Such mappings can occur both in the store and in the component (or in another consumer of the store). This depends on how local or global the store and the data to be derived are.

The bottom line is that the consistent use of this approach supports the so-called unidirectional data flow, which makes system behavior more understandable. The following listing demonstrates this from the perspective of a component that relies on the NGRX Signal Store.

1 @Component([…]) 2 export class DessertsComponent { 3 #store = inject(DessertStore); 4 5 originalName = this.#store.filter.originalName; 6 englishName = this.#store.filter.englishName; 7 8 ratedDesserts = this.#store.ratedDesserts; 9 loading = this.#store.loading; 10 11 constructor() { 12 this.#store.loadDesserts(); 13 } 14 15 search(): void { 16 this.#store.loadDesserts(); 17 } 18 19 loadRatings(): void { 20 this.#store.loadRatings(); 21 } 22 23 updateRating(id: number, rating: number): void { 24 this.#store.updateRating(id, rating); 25 } 26 27 updateFilter(filter: DessertFilter): void { 28 this.#store.updateFilter(filter); 29 } 30 }



Since the component only has to delegate to the store, it is very straightforward.





Conclusion


To truly leverage the benefits of Signals, the application must be designed as a reactive system. This means, among other things, that writing values is avoided in favor of deriving values from existing ones. This simplifies the program code, especially since derived values are automatically kept up to date.

Signals are currently primarily suitable for transporting data into the view. Effects are used when API calls are necessary for that, e.g. when displaying a toast. The current Signals implementation is not intended to trigger asynchronous actions. Instead, classic events or observables are the way to go. Stores that can also handle asynchronous operations help establish unidirectional data flow and make reactive applications more manageable.





Built-in Control Flow and Deferrable Views


Angular 17 introduced a new template syntax for control flow bocks. Also, the same syntax is used to provide deferrable views. These are lazy loaded parts of a page that help to improve the initial load performance.





New Syntax for Control Flow in Templates


Since its early days, Angular has used structural directives like *ngIf or *ngFor for control flow. Since the control flow needs to be revised anyway to allow for the envisioned fine-grained change detection and for eventually going Zone-less, the Angular team has decided to put it on a new footing. The result is the new build-in control flow, which stands out clearly from the rendered markup:

1 @for (product of products(); track product.id) { 2 <div class="card"> 3 <h2 class="card-title">{{product.productName}}</h2> 4 […] 5 </div> 6 } 7 @empty { 8 <p class="text-lg">No Products found!</p> 9 }



One thing worth noting here is the new @empty block that Angular renders when the collection being iterated is empty.

Although signals were a driver for this new syntax, they are not a requirement for its use. The new control flow blocks can also be used with classic variables or with observables in conjunction with the async pipe.

The mandatory track expression allows Angular to identify individual elements that have been moved within the iterated collection. This enables Angular (to be more precise: Angular’s new reconciliation algorithm) to drastically reduce the rendering effort and to reuse existing DOM nodes. When iterating over collections of primitive types, e.g. Arrays with numbers or strings, track could point to the pseudo variable $index according to information from the Angular team:

1 @for (group of groups(); track $index) { 2 <a (click)="groupSelected(group)">{{group}}</a> 3 @if (!$last) { 4 <span class="mr-5 ml-5">|</span> 5 } 6 }



In addition to $index, the other values known from *ngFor are also available via pseudo variables: $count, $first, $last, $even, $odd. If necessary, their values can be stored in template variables too:

1 @for (group of groups(); track $index; let isLast = $last) { 2 <a (click)="groupSelected(group)">{{group}}</a> 3 @if (!isLast) { 4 <span class="mr-5 ml-5">|</span> 5 } 6 }



The new @if simplifies the formulation of else/ else-if branches:

1 @if (product().discountedPrice && product().discountMinCount) { 2 […] 3 } 4 @else if (product().discountedPrice && !product().discountMinCount) { 5 […] 6 } 7 @else { 8 […] 9 }



In addition, different cases can also be distinguished using a @switch:

1 @switch (mode) { 2 @case ('full') { 3 	 […] 4 } 5 @case ('small') { 6 […] 7 } 8 @default { 9 […] 10 } 11 }



Unlike ngSwitch and *ngSwitchCase, the new syntax is type-safe. In the example shown above, the individual @case blocks must have string values, since the mode variable passed to @switch is also a string.

The new control flow syntax reduces the need to use structural directives, which are powerful but sometimes unnecessarily complex. Nevertheless, the framework will continue to support structural directives. On the one hand, there are some valid use cases for it and on the other hand, despite the many exciting innovations, the framework needs to be made backwards compatible.





Automatic Migration to Build-in Control Flow


If you would like to automatically migrate your program code to the new control flow syntax, you will now find a schematic for this in the @angular/core package:

1 ng g @angular/core:control-flow





Delayed Loading


Typically, not all areas of a page are equally important. A product page is primarily about the product itself. Suggestions for similar products are secondary. However, this changes suddenly as soon as the user scrolls the product suggestions into the visible area of the browser window, the so-called view port.

For particularly performance-critical web applications such as web shops, it makes sense to defer loading less important page parts. This means that the really important elements are available more quickly. Until now, anyone who wanted to implement this idea in Angular had to do it manually. Angular 17 also dramatically simplifies this task with the new @defer block:

1 @defer (on viewport) { 2 <app-recommentations [productGroup]="product().productGroup"> 3 </app-recommentations> 4 } 5 @placeholder { 6 <app-ghost-products></app-ghost-products> 7 }



Using @defer delays the loading of the enclosed page part until a certain event occurs. As a replacement, it presents the placeholder specified under @placeholder. In the demo application used here, ghost elements are first presented for the product suggestions:

Ghost Elements as placeholders

Once loaded, @defer swaps the ghost elements for the actual suggestions:

@defer swaps the placeholder for the lazy-loaded component

In the discussed example, the on viewport event is used. It occurs once the placeholder has been scrolled into view. Besides this event, there are several other options too:

Triggers Description

on idle The browser reports that there are no critical tasks pending (default).

on viewport The placeholder is loaded into the visible area of the page.

on interaction The user begins to interact with the placeholder.

on hover The mouse cursor is moved over the placeholder.

on immediate As soon as possible after the page loads.

on timer(duration) After a certain time, e.g. on timer(5s) to trigger loading after 5 seconds.

when condition Once the specified condition is met, e.g. when (userName !=== null)

By default, on viewport, on interaction, and on hover force a @placeholder block to be specified. Alternatively, they can also refer to other page parts that can be referenced via a template variable:

1 <h1 #recommentations>Recommentations</h1> 2 @defer (on viewport(recommentations)) { 3 <app-recommentations […] /> 4 }



Additionally, @defer can be told to preload the bundle at an earlier time. As with preloading routes, this approach ensures that bundles are available as soon as you need them:

1 @defer(on viewport; prefetch on immediate) { […] }



In addition to @placeholder, @defer also offers two other blocks: @loading and @error. Angular displays the former one while it loads the bundle; the latter one is shown in the event of an error. To avoid flickering, @placeholder and @loading can be configured with a minimum display duration. The minimum property sets the desired value:

1 @defer ( […] ) { 2 […] 3 } 4 @loading (after 150ms; minimum 150ms) { 5 […] 6 } 7 @placeholder (minimum 150ms) { 8 […] 9 }



The after property also specifies that the loading indicator should only be displayed if loading takes longer than 150 ms.





Conclusion


The new Built-in Control Flow Blocks stand out visually from the template’s markup. The also pave the way for incrementally updating components. Deferrable Views use the same syntax and allow to postpone loading specific elements of a page to improve loading times.





esbuild and the new Application Builder


The new esbuild support provides faster builds. The subsequent ApplicationBuilder streamlines the use of SSR.





Build Performance with esbuild


Originally, the Angular CLI used webpack to generate bundles. However, webpack is currently being challenged by newer tools that are easier to use and a lot faster. esbuild is one of these tools that, with over 20,000 downloads per week, has a remarkable distribution.

The CLI team has been working on an esbuild integration for several releases. In Angular 16, this integration was already included as a developer preview. As of Angular 17, this implementation is stable and used by default for new Angular projects via the Application Builder described below.

For existing projects, it is worth considering switching to esbuild. To do this, update the builder entry in angular.json:

1 "builder" : "@angular-devkit/build-angular:browser-esbuild"



In other words: -esbuild must be added at the end. In most cases, ng serve and ng build should behave as usual, but be a lot faster. The former uses the vite dev server to speed things up by only building npm packages when needed. In addition, the CLI team integrated several additional performance optimizations.

Calling ng build is also drastically accelerated by using esbuild. Factor 2 to 4 is often mentioned as the range.





SSR Without Effort with the new Application Builder


Support for server-side rendering (SSR) has also been drastically simplified with Angular 17. When generating a new project with ng new, a –ssr switch is now available. If this is not used, the CLI asks whether it should set up SSR:

ng new sets up SSR if desired

To enable SSR later, all you need to do is to add the @angular/ssr package:

1 ng add @angular/ssr



The @angular scope makes clear, this package comes directly from the Angular team. It is the successor to the community project Angular Universal. To directly take SSR into account during ng build and ng serve, the CLI team has provided a new builder. This so-called application builder uses the esbuild integration mentioned above and creates bundles that can be used both in the browser and on the server side.

A call to ng serve also starts a development server, which both renders on the server side and delivers the bundles for operation in the browser. A call to ng build –ssr also takes care of bundles for both worlds as well as building a simple Node.js-based server whose source code uses the schematics mentioned above.

If you can’t or don’t want to run a Node.js server, you can use ng build –prerender to prerender the individual routes of the application during build.





More than SSR: Non-destructive Hydration


Single page applications (SPAs) enable good runtime performance. However, the initial page load usually takes a few seconds longer than with classic web applications. This is because the browser has to load large amounts of JavaScript code in addition to the actual HTML page before it can render the page. The so-called First Meaningful Paint (FMP) only takes place after a few seconds:

Client-side rendering of a SPA

While these few seconds are hardly an issue in business applications, they actually pose a problem for public web solutions such as web shops. Here it is important to keep the bounce rate low and this can be achieved, among other things, by keeping waiting times as short as possible.

It is therefore common to render SPAs for such scenarios on the server side so that the server can already deliver a finished HTML page. The caller is thus quickly presented with a page. Once the JavaScript bundles have loaded, the page is also interactive. The next image illustrates this: The First Meaningful Paint (FMP) now takes place earlier. However, the site will only become interactive later (Time to Interactive, TTI).

Server-side rendering of an SPA

To support solutions where the initial page load matters, Angular has offered server-side rendering (SSR) since its early days. However, the behavior of this SSR implementation has been “destructive” in the past. This means that the loaded JavaScript code re-rendered the entire page. All server-side rendered markup was replaced with client-side rendered markup. Unfortunately, this is also accompanied by a slight delay and flickering. Metrics show that this degrades startup performance.

Angular 16 also addresses this issue by reusing the already server-side rendered markup from the JavaScript bundles loaded into the browser. We are talking about non-destructive hydration here. The word hydration describes the process that makes a loaded page interactive using JavaScript.

To use this new feature, first install the @nguniversal/express-engine package for SSR support:

1 ng add @nguniversal/express-engine



After that, non-destructive hydration is enabled with the standalone API provideClientHydration:

1 // app.config.ts 2 export const appConfig: ApplicationConfig = { 3 providers: [ 4 provideClientHydration(), 5 ] 6 };



The listing sown takes care of this in the app.config.ts file . The structure of the ApplicationConfig type published there is used in the main.ts file when bootstrapping the application. Incidentally, the app.config.ts file is set up by the CLI when a new application is set up with the --standalone switch.

To debug an application that relies on SSR or hydration, the using schematics set up the npm script ssr:dev:

1 npm run ssr:dev



Behind it is a development server that was developed by an extremely charming Austrian collaborator and runs the application in debug mode on both the server and client side.





More Details on Hydration in Angular


If the SPA calls Web APIs via HTTP during server-side rendering, the responses received are also automatically sent to the browser via a JSON fragment within the rendered page. When hydrating, the HttpClient in the browser uses this fragment instead of making the same request again. With this, Angular speeds up hydration. If this behavior is not desired, it can be deactivated with the withNoHttpTransferCache function:

1 provideClientHydration( 2 withNoHttpTransferCache() 3 ),



For non-destructive hydration to work, the markup rendered on the server side must match the markup on the client side. This cannot always be guaranteed, especially with third-party components or when using libraries that manipulate the DOM directly. In this case, however, non-destructive hydration can be deactivated for individual components with the ngSkipHydration attribute:

1 <app-flight-card 2 ngSkipHydration 3 [item]="f" 4 [(selected)]="basket()[f.id]" />



Angular does not allow data binding for this attribute. Also, Angular expects ngSkipHydration to be either zero or true. If you want to generally exclude hydration for a component, you can also set this attribute via a host binding:

1 @Component({ 2 […] 3 host: { 'ngSkipHydration': 'true' } 4 })



If several Angular applications run in parallel on one side, Angular must be able to distinguish between these applications using an ID. The token APP_ID is used for this:

1 { provide: APP_ID, useValue: 'myApp' },



The result of the new non-destructive hydration is quite impressive. The following two figures show some Lighthouse metrics for the example application used here. The former refers to classic SSR and the latter to the new non-destructive hydration.

Classic SSR

SSR with Non-Destructive Hydration

Apart from creating a production build and enabling HTTP compression in the node-based web server responsible for server-side rendering, no optimizations have been implemented.





Conclusion


The new esbuild-based ApplicationBuilder accelerates the build process noticeably and directly supports Server-side Rendering (SSR). Together with Hydration, SSR improves the startup times of a page.





About the Author


Manfred Steyer

Manfred Steyer is a trainer, consultant, and programming architect with focus on Angular.

For his community work, Google recognizes him as a Google Developer Expert (GDE). Also, Manfred is a Trusted Collaborator in the Angular team. In this role he implemented differential loading for the Angular CLI.

Manfred wrote several books, e. g. for O’Reilly, as well as several articles, e. g. for the German Java Magazine, windows.developer, and Heise.

He regularly speaks at conferences and blogs about Angular.

Before, he was in charge of a project team in the area of web-based business applications for many years. Also, he taught several topics regarding software engineering at a university of applied sciences.

Manfred has earned a Diploma in IT- and IT-Marketing as well as a Master’s degree in Computer Science by conducting part-time and distance studies parallel to full-time employments.

You can follow him on Twitter and Facebook and find his blog here.





Trainings and Consulting


If you and your team need support or trainings regarding Angular, we are happy to help with workshops and consulting (on-site or remote). In addition to several other kinds of workshop, we provide the following ones:

Advanced Angular: Enterprise Solutions and Architecture

Angular Essentials: Building Blocks and Concepts

Modern Angular Workshop

Angular Micro Frontends Workshop

Angular Testing Workshop (Cypress, Jest, etc.)

Angular Performance Workshop

Angular Design Systems Workshop (Figma, Storybook, etc.)

Angular: Reactive Architectures (RxJS and NGRX)

Angular Review Workshop

Angular Upgrade Workshop



Please find the full list of our offers here.

Modern Angular Workshop

Modern Angular (English) | Modern Angular (German)

We provide our workshops and consulting in various forms: remote or on-site; public or as dedicated company workshops; in English or in German.

If you have any questions, reach out to us at <PRESIDIO_ANONYMIZED_EMAIL_ADDRESS>.





" can you help me understand defer?