Angular Revisited: Standalone Angular applications, the replacement for NgModules

layzee

Lars Gyrup Brink Nielsen

Posted on August 30, 2022

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

💖 💪 🙅 🚩
layzee
Lars Gyrup Brink Nielsen

Posted on August 30, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related