Cover photo by Laura Cleffman on Unsplash.

Four years have passed since I first began exploring standalone Angular applications—those that, unlike classic Angular apps, operate entirely without Angular modules.

With Angular version 15, the framework delivers a fully realized standalone application experience, and this shift goes far beyond merely introducing standalone components. It represents a fundamental reframing of how we understand core Angular concepts.

Optional NgModules

Standalone Angular applications complete the saga of optional NgModules. We are no longer compelled to leverage or author Angular modules. Every scenario now offers an alternative.

Angular modules stand as one of the least intuitive aspects of the Angular framework. The original ambition for Angular was to discard Angular modules following the AngularJS 1.x era, yet just prior to the Angular 2.0 release, Angular modules were reintroduced to assist the compiler, enabling application-level Ahead-of-Time compilation — a substantial enhancement over Just-in-Time compilation, which was the sole compilation method for AngularJS.

Angular modules are challenging to instruct and comprehend. Presented as compiler annotations out of necessity rather than for developer convenience, Angular modules cater to numerous issues, with connecting declarables to component templates and setting up the environment injector standing out as the primary 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

We will examine each metadata option for the NgModule decorator, clarify their function, and identify the corresponding standalone application solutions.

NgModule.bootstrap

Designates one or more components to be launched as root components.

Swap this out with bootstrapApplication.

NgModule.declarations

Registers components, directives, and pipes, incorporating them into the Angular module's transitive compilation scope.

⚠️ Warning
A conventional component, directive, or pipe is restricted to a single Angular module. Associating them with multiple Angular modules triggers compilation errors.

Use the Component.imports, Component.standalone, Directive.standalone, and Pipe.standalone metadata options as substitutes.

NgModule.entryComponents

Marked as deprecated in Angular version 9, which was the inaugural stable release of Angular Ivy, the NgModule.entryComponents option designates a component for dynamic rendering capabilities. This was handled automatically for components indicated with NgModule.bootstrap and Route#component during the Angular Template Compiler and Angular View Engine framework periods.

ℹ️ Note
Discontinue using this metadata option in conventional Angular applications.

Within Angular Ivy, components do not require explicit marking as entry components. All components can be rendered dynamically, thus no alternative is needed.

NgModule.exports

Identifies classic and/or standalone declarables as part of this Angular module's transitive exported scope. Including other Angular modules incorporates their transitive exported scope into this Angular module's transitive exported scope.

Employ the native export statement to make a standalone declarable accessible to the template of a component that includes it through the Component.imports metadata option, or the transitive scope of an Angular module that references it via the NgModule.imports or NgModule.exports metadata options.

For signaling public versus internal availability of a standalone declarable, we can arrange our Angular workspaces with barrel files, workspace libraries, or lint rules.

NgModule.id

Flags this Angular module as non-tree-shakable and permits retrieval through the getNgModuleById function.

⚠️ Warning
This option is likely unnecessary for you.

Not necessary in standalone applications. For conventional applications, use a dynamic import statement as an alternative, for example:

const TheModule = await import('./the.module')
  .then(esModule => esModule.TheModule);
Enter fullscreen mode Exit fullscreen mode

NgModule.jit

Omits this Angular module and its declarations from Ahead-of-Time compilation.

ℹ️ Note
For this option to take effect, the JIT compiler must be included with the application, such as by adding the statement below in the main.ts file:

import '@angular/compiler';

Introduced in Angular version 6 to facilitate the ongoing efforts for the subsequent framework generation, which was Angular Ivy.

Swap this option with the Component.jit and Directive.jit metadata options.

NgModule.imports

Incorporates the transitive exported scope of the listed Angular modules into this Angular module's transitive module scope. Standalone declarables may be listed as well to embed them into this Angular module's transitive module scope.

This action connects the imported declarables to the templates of components declared by this Angular module.

Providers specified in Angular modules added to the NgModule.imports metadata option become part of the environment injector(s) (previously known as module injectors) to which this Angular module belongs.

For designating components, directives, and pipes as declarable dependencies of a standalone component, utilize its Component.imports metadata option, which also accommodates Angular modules.

NgModule.providers

Enumerates providers that are attached to the environment injector(s) (formerly known as module injectors) associated with this Angular module.

Angular version 6 brought tree-shakable providers, eliminating the requirement for Angular modules to set up environment injectors, which were called module injectors at that time.

Substitute NgModule.providers with the InjectionToken.factory metadata option, the Injectable.providedIn metadata option, the Route#providers setting, and the ApplicationConfig#providers setting.

💡 Tip
Think about adopting a provider at the component level to align with the lifecycle of a directive or component by setting the Component.providers, Component.viewProviders, and Directive.providers metadata options. Apply this approach in both classic and standalone Angular applications.

NgModule.schemas

Introduces template compilation schemas to permit web component usage by including the CUSTOM_ELEMENTS_SCHEMA, or to enable the dismissal of any unfamiliar element, attribute, or property by including the NO_ERRORS_SCHEMA.

This governs the template compilation schemas for components that are declared by this Angular module.

Replace this with the Component.schemas metadata option.

As shown throughout this introductory piece, every conceivable Angular module metadata option now has a corresponding replacement in a standalone Angular application.

Standalone equivalents for the standard Angular modules

The public APIs of the official Angular packages expose a number of Angular modules. Since Angular version 15.1, standalone replacements for the following standard Angular modules are available:

ℹ️ Note
The traditional Angular modules remain fully functional and are not marked as deprecated.

Standalone vs. conventional Angular applications

Thanks to interoperability between standalone APIs and Angular modules, switching a standalone Angular application does not require a full-scale rewrite. You can either incrementally adopt standalone APIs, apply them only to new features, or keep the existing classic Angular APIs untouched for the time being.

When Angular 15.x was current, the decision between a standalone or a classic Angular application was largely a matter of preference. Still, some noteworthy differences have already emerged:

  • bootstrapApplication and createApplication lack support for NgZone options, which PlatformRef#bootstrapModule does have, meaning these standalone starting points cannot be used to keep Zone.js out of the application bundle
  • The Directive composition API only accepts standalone directives and components as host directives
  • Standalone APIs are simpler to grasp and explain, as they add less cognitive load and rely on native JavaScript structures instead of framework-specific metadata
  • Automatic imports in the Angular Language Service are available exclusively for standalone components
  • Writing component tests is less complicated with standalone declarables
  • Standalone declarables likewise simplify Storybook stories
  • Lazy loading and dynamic rendering of standalone components work through ViewContainerRef#createComponent
  • Standalone components can be wrapped inside React, as shown by the ngx-reactify proof-of-concept Gist
  • Astro is able to render and hydrate standalone components via a plugin provided by Analog.js
  • @defer blocks restrict their deferrable views to standalone components only

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