Cover photo by Laura Cleffman on Unsplash.
It's been 4 years since I started looking into standalone Angular applications, that is Angular applications that unlike classic Angular applications have no Angular modules.
Angular version 15 delivers an amazing full-on standalone Angular application experience and it is about much more than standalone components. It is a shift in perspective on Angular concepts as we know them.
Optional NgModules
Standalone Angular applications mark the final milestone of the optional NgModules epic. No longer do we have to use or write Angular modules. We now have an alternative for every use case.
Angular modules are one of the most confusing concepts of the Angular framework. The vision for Angular was to get rid of Angular modules following AngularJS versions 1.x but shortly before Angular version 2.0, Angular modules were reintroduced for the sake of the compiler to pave the path for application-scoped Ahead-of-Time compilation, a major improvement compared to Just-in-Time compilation, the only compilation mode for AngularJS.
Angular modules are difficult to teach and learn. Introduced as necessary compiler annotations rather than to improve the developer experience, Angular modules address many concerns with declarable linking to component templates and environment injector configuration being the two major concerns.
@NgModule({
bootstrap: [AppComponent],
declarations: [AppComponent],
entryComponents: [AppComponent],
exports: [AppComponent],
id: 'app',
imports: [
BrowserAnimationsModule,
HttpClientModule,
CommonModule,
MatButtonModule,
RouterModule.forRoot(routes),
],
jit: false,
providers: [AppService],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
Let's have a look at every metadata option for the NgModule
decorator, discuss their purpose and their standalone application replacements.
NgModule.bootstrap
Marks one or more components to be bootstrapped as root components.
Replace with bootstrapApplication
.
NgModule.declarations
Declares components, directives, and pipes, including them in Angular module's transitive compilation scope.
⚠️ Warning
A classic component, directive, or pipe can only be declared in one Angular module. Declaring them in multiple Angular modules results in compilation errors.
Replace with the Component.imports
, Component.standalone
, Directive.standalone
, and Pipe.standalone
metadata options.
NgModule.entryComponents
Deprecated since Angular version 9, the first stable release of Angular Ivy, the NgModule.entryComponents
option marks a component for dynamic rendering support. This was implicitly done for components marked with NgModule.bootstrap
and Route#component
in the Angular Template Compiler and Angular View Engine framework generations.
ℹ️ Note
Stop using this metadata option in classic Angular applications.
In Angular Ivy, components do not have to be marked explicitly as entry components. Dynamic rendering of any component is possible so no replacement is necessary.
NgModule.exports
Marks classic and/or standalone declarables as part of this Angular module's transitive exported scope. Listing other Angular modules includes their transitive exported scope in this Angular module's transitive exported scope.
Replace with the native export
declaration to make a standalone declarable accessible to the template of a component including it in its Component.imports
metadata option or the transitive scope of an Angular module including it in its NgModule.imports
or NgModule.exports
metadata options.
To indicate public or internal access to a standalone declarable, we can structure our Angular workspaces using barrel files, workspace libraries, and or lint rules.
NgModule.id
Marks this Angular module as non-tree-shakable and allows access through the getNgModuleById
function.
⚠️ Warning
You probably don't need this option.
Not needed in standalone applications. For classic application, replace with a dynamic import statement, for example:
const TheModule = await import('./the.module')
.then(esModule => esModule.TheModule);
NgModule.jit
Excludes this Angular module and its declarations from Ahead-of-Time compilation.
ℹ️ Note
The JIT compiler must be bundled with the application for this option to work for example by adding the following statement in themain.ts
file:import '@angular/compiler';
Introduced in Angular version 6 to support the ongoing work of what was going to be the next framework generation, Angular Ivy.
Replace with the Component.jit
and Directive.jit
metadata options.
NgModule.imports
Includes the transitive exported scope of listed Angular modules in this Angular module's transitive module scope. Standalone declarables can also be listed to include them in this Angular module's transitive module scope.
This links imported declarables to templates of components declared by this Angular module.
Providers listed in Angular modules added to the NgModule.imports
metadata option are added to the environment injector(s) (formerly known as module injectors) that this Angular module is part of.
To mark components, directives, and pipes as declarable dependencies of a standalone component, use its Component.imports
metadata option which also supports Angular modules.
NgModule.providers
Lists providers that are added to the environment injector(s) (formerly known as module injectors) that this Angular module is part of.
Angular version 6 introduced tree-shakable providers, removing the need for Angular modules to configure environment injectors, at the time known as module injectors.
Replace NgModule.providers
with the InjectionToken.factory
metadata option, Injectable.providedIn
metadata option, Route#providers
setting, and ApplicationConfig#providers
setting.
💡 Tip
Consider using a component-level provider to follow the lifecycle of a directive or component by specifying theComponent.providers
,Component.viewProviders
, andDirective.providers
metadata options. Consider this both for classic and standalone Angular applications.
NgModule.schemas
Adds template compilation schemas to support web component usage by listing the CUSTOM_ELEMENTS_SCHEMA
or to ignore the use of any unknown element, attribute, or property by listing the NO_ERRORS_SCHEMA
.
This controls the template compilation schemas for components that are declared by this Angular module.
Replace with the Component.schemas
metadata option.
As we have learned in this introductory article, every possible Angular module metadata option now has a standalone Angular application replacement.
Standalone alternatives for official Angular modules
Several Angular modules are exposed in the public APIs of official Angular packages. As of Angular version 15.1, there are standalone alternatives for the following official Angular modules:
ℹ️ Note
The classic Angular modules can still be used and are not deprecated.
Standalone vs. classic Angular applications
Due to interoperability between standalone APIs and Angular modules, standalone Angular applications do not require a big bang migration. We can gradually migrate to standalone APIs or use standalone APIs for new features but leave the classic Angular APIs in-place for now.
In Angular version 15.x, picking between standalone and classic Angular applications was mostly a stylistic choice. However, there are already noteworthy differences to consider:
-
bootstrapApplication
andcreateApplication
do not support NgZone options unlikePlatformRef#bootstrapModule
, making it impossible to exclude Zone.js from our standalone application bundle using these APIs - The Directive composition API only supports standalone directives and components as host directives
- Standalone APIs are easier to teach and learn because of less mental overhead and simpler APIs using native data structures without framework-specific metadata
- The Angular Language Service only supports automatic imports for standalone components
- Component testing is easier with standalone declarables
- Storybook stories are easier with standalone declarables
- Standalone components can be lazy-loaded and dynamically rendered using ViewContainerRef#createComponent
- Standalone components can be wrapped in React components as demonstrated by the ngx-reactify proof-of-concept Gist
- Standalone components can be rendered and hydrated by Astro by using a plugin by Analog.js
-
@defer
blocks only support standalone components in their derrable views
You take the blue pill, the story ends, you wake up in your bed and believe whatever you want to believe. You take the red pill, you stay in wonderland, and I show you how deep the rabbit hole goes.
—Morpheus
Top comments (3)
One question is interesting to me with this awesome Angular update: are there any significant performance differences between the two approaches? Or it’s only a matter of mental models and levels of abstractions at this stage?
My guess it's negligible but I imagine there could be a difference in compilation speed between a classic (NgModule-based) Angular application and a standalone Angular application.
The Angular team has suggested that a standalone component introduces a special injector. I'm not sure if or how this will affect runtime performance.
With the latest updates ,Angular is trying to achieve more of the functional approach. By introducing the functional based directives , resolvers, guards, it has opened the door for the component to be more independent, segregated. This allows the code to be more flexible. Tree shaking becomes easy as the code is not tightly bound with this approach.
For ex: The injector function eliminates the dependency for the service to be added in the constructor . This make the inheritance easy. A class can be inherited without using the super constructor to be called if the base class is using any service.
Also, the dependency can be lazy loaded based on its usage.
All these powers up the component to stand alone firmly!