
Angular’s Future Without NgModules – Part 1: Lightweight Solutions Using Standalone Components
Standalone Components make the future of Angular applications more lightweight. We don't need NgModules anymore. Instead, we just use EcmaScript modules.
Update on 2022-05-01: Texts and examples fully updated to use initial Standalone Components support in Angular 14.0.0-next.15 (instead of just using a shim for simulating them).
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. A first implementation already landed in Angular 14 BETA and the Angular Team tries hard to make them available until version 14 is released in the first half of 2022.
In this article series, 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 (see several branches) and as an Nx workspace that uses libraries as a replacement for NgModules.
A Bit of History: NgModules – Unwanted but Needed
Actually, Angular modules that were already used in AngularJS 1.x should not be implemented at all. Rather, the core team was happy to announce its discontinuation in 2014 when Angular 2 was announced in Paris. In a presentation that will probably go down in the history of the framework, a tombstone was shown for every concept from AngularJS 1.x that was no longer required. The tenor was that all of these concepts had become obsolete: partly because of the new architecture of Angular, partly because certain concepts were already planned for the next iteration of EcmaScript.
The latter was the case with NgModules, which appeared obsolete due to the module system planned for EcmaScript 2015. The community was all the more surprised when one of the last release candidates appeared with NgModules. The main reason for this 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.
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:
import { Component } from '@angular/core';
import { NavbarComponent, SidebarComponent } from './shell';
import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
selector: 'app-root',
imports: [
RouterModule,
CommonModule,
NavbarComponent,
SidebarComponent,
HomeComponent,
],
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
[...]
}
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:
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:
@Pipe ({
standalone: true,
name: 'city',
pure: true
})
export class CityPipe implements PipeTransform {
transform (value: string, format: string): string {[…]}
}
And here is an example for a standalone directive:
@Directive ({
standalone: true,
selector: 'input [appCity]',
providers: […]
})
export class CityValidator implements Validator {
[...]
}
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:
@Injectable ({
providedIn: 'root'
})
export class FlightService {[…]}
In a later part of this article series, we will look more closely to dependency injection in a world of Standalone Components. However, to anticipate one key point already now: Using modern tree-shakable providers instead of old-style providers in NgModules helps a lot when migrating to Standalone Components.
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
:
// main.ts
import { HttpClientModule } from '@angular/common/http';
import { enableProdMode, importProvidersFrom } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app/app.component';
import { APP_ROUTES } from './app/app.routes';
import { TicketsModule } from './app/tickets/tickets.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(RouterModule.forRoot(APP_ROUTES)),
importProvidersFrom(HttpClientModule),
importProvidersFrom(TicketsModule),
]
});
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 bridge the gap to existing NgModules. The spread operator used here will become optional quite soon.
Please also note, that importProvidersFrom
works with both NgModules but also ModuleWithProviders
as returned by the Router’s forRoot
and forChild
methods. 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 Angular team plans to introduce a provideRouter
function:
bootstrapApplication(AppComponent, {
providers: [
provideRouter(APP_ROUTES)
[...]
]
});
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:
@Component({
standalone: true,
selector: 'app-root',
imports: [
RouterModule,
CommonModule,
[...]
],
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
[...]
}
But on the other side, we can also import a Standalone Component (Directive, Pipe) into an existing NgModule:
@NgModule({
imports: [
CommonModule,
// Imported Standalone Component:
FlightCardComponent,
[...]
],
declarations: [
MyTicketsComponent
],
[...]
})
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 an 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.
Interim Conclusion: Standalone Components — and now?
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 part of this short series will shed some light on this.
» Next Part: Angular’s Future Without NgModules – Part 2: What Does That Mean for Our Architecture?
More on Architecture?
When architecting enterprise-scale Angular applications, several additional questions come in mind:
- According to which criteria can we sub-divide a huge application into libraries and sub-domains?
- Which access restrictions make sense?
- Which proven patterns should we use?
- How can we evolve our solution towards micro frontends?
Our free eBook (about 100 pages) covers all these questions and more:
Feel free to download it here now!
Unsere Angular-Schulungen
