DEV Community

Cover image for Angular Revisited: Standalone Angular applications, the replacement for NgModules
Lars Gyrup Brink Nielsen for This is Angular

Posted on • Updated on

Angular Revisited: Standalone Angular applications, the replacement for NgModules

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 {}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 the main.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 the Component.providers, Component.viewProviders, and Directive.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 and createApplication do not support NgZone options unlike PlatformRef#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

Red pill: Standalone. Blue pill: NgModules.

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)

Collapse
 
fyodorio profile image
Fyodor

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?

Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen

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.

Collapse
 
codecraftjs profile image
Code Craft-Fun with Javascript

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!