1. Angular’s Future Without NgModules – Part 1: Lightweight Solutions Using Standalone Components
  2. Angular’s Future Without NgModules – Part 2: What Does That Mean for Our Architecture?

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.

  1. Angular’s Future Without NgModules – Part 1: Lightweight Solutions Using Standalone Components
  2. Angular’s Future Without NgModules – Part 2: What Does That Mean for Our Architecture?

They have always been one of the most controversial building blocks in Angular: Angular Modules, or NgModules for short. Some like modules because they allow related building blocks such as components, pipes or directives to be grouped together. The others want a future without modules and instead just go with the module system offered by EcmaScript.

The Angular team is now taking on this task and works on making Angular modules optional. A design document for standalone components and an associated RFC (Request for Comments) has been around for some time. In addition, the Angular team also offers a shim that allows you to try out the planned ideas already today. Although this shim is not intended for productive use, it shows us what a world without NgModuls will feel like.

I updated one of our workshop applications based on this shim. The resulting implementation shows how the elimination of NgModules can affect our future Angular architectures. The source code for this can be found in the form of a CLI workspace and as an Nx workspace that uses libraries as a replacement for NgModules.

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.

The Mental Model

The mental model behind Standalone Components is actually simple. Imagine a component that has its own NgModule:

[...]
import {Component} from './standalone-shim';
import {NavbarComponent, SidebarComponent} from './shell';

@Component ({
  standalone: ​​true,
  selector: 'app-root',
  imports: [
    NavbarComponent,
    SidebarComponent,
    HomeComponent,
    AboutComponent,
    HttpClientModule,
    RouterModule.forRoot ([...])
  ],
  template: `[...]`
})
export class AppComponent {
}

This is similar as Lars Nielsen‘s SCAM pattern. However, while SCAM uses an explicit module, here we only talk about a thought one.

The standalone: ​​true flag indicates that this is a standalone component. The new property imports defines the compilation context, i.e. the number of all building blocks that the component is allowed to use. This can be used to import other standalone components, but also existing NgModules.

The exhaustive listing of all these building blocks makes the component survivable on its own 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.

Compatibility With Existing Code

The decision to treat standalone components like modules, among other things, is also the key to compatibility with the existing eco-system. This means that existing modules can be integrated into standalone components. The component in the former listing imports, for example, the RouterModule including configuration and the HttpClientModule in this way.

But also from the point of view of existing code sections that use NgModules, the mental model ensures compatibility, especially since Angular modules can also import standalone components:

@NgModule ({
    imports: [SomeStandaloneComponent],
    exports: [...],
    declarations: […],
    providers: […],
})
export class SomeModule {}

It is interesting here that standalone components are imported like modules and not declared like classic components. This may be confusing, but it fits the mental model that views a standalone component as both a component and a reusable NgModule.

Bootstrapping Standalone Components

Until now, modules were also required for bootstrapping, especially since the functions provided by Angular for this purpose expected a module with a bootstrap component. The application thus provided both the main component and its compilation context. In the future, it will be possible to bootstrap a a single component. The shim provides a method bootstrapComponent for this purpose, which can be used in main.ts:

bootstrapComponent ( AppComponent );

Standalone Components and Lazy Loading

The discussed mental model also ensures that the lazy loading of standalone components works with the router. The router is currently capable of obtaining modules via lazy loading. Since a standalone component is also viewed as a module, it already works now:

{
  path: 'flight-booking',
  loadChildren: () =>
      import ('./ booking / flight-booking.component')
        .then (m => m.FlightBookingComponent ['module'])
         // module property: workaround in shim ^^ 
}

The module property shown here is just an auxiliary construct of the shim used, which actually explicitly creates one module per standalone building block under the hoods. However, the actual implementation will do without this crutch. In addition, there are already ideas to give the router some fine-tuning so that it can better interact with standalone components.

Since the introduction of Ivy, individual components can also be obtained via lazy loading:

@Component ({
  standalone: ​​true,
  selector: 'app-about',
  template: `
    <h1> About </h1>
    <ng-container #container> </ng-container>
  `
})
export class AboutComponent {

  @ViewChild ('container', {read: ViewContainerRef})
  viewContainer!: ViewContainerRef;

  async ngOnInit () {
    const esm = await import ('./ lazy / lazy.component');
    const ref = this.viewContainer.createComponent (esm.LazyComponent)
    ref.instance.title = `I'm so lazy today !!`;
  }

}

The example loads the component with a dynamic import and instantiates it within the ViewContainerRef of a placeholder. Before Angular 13, the application had to get a factory for the loaded component. In the meantime, createComponent accepts the component class directly.

In principle, this approach has been possible since Ivy was released. However, the compilation context of the component was lost if the associated module was not stored in the same file. However, since a standalone component defines its context itself, this workaround is no longer necessary.

Pipes, Directives, and Services

Analogous to standalone components, standalone pipes and standalone directives can also be used. For this purpose, the pipe and directive decorators will 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 addition, it has always been possible to set up a provider for a component. The set up service applies to this component and all those in the component tree below. The providers property of the component decorator is used for this:

@Component ({
  standalone: ​​true,
  imports: [...],

  // Define a provider for the component:
  providers: [FlightService],

  selector: 'flight-search',
  template: `[...]`
})
export class FlightSearchComponent {[…]}

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:

free ebook

Feel free to download it here now!

Don't Miss Anything!


Subscribe to our newsletter to get all the information about Angular.


* By subscribing to our newsletter, you agree with our privacy policy.

Unsere Angular-Schulungen

  1. Angular’s Future Without NgModules – Part 1: Lightweight Solutions Using Standalone Components
  2. Angular’s Future Without NgModules – Part 2: What Does That Mean for Our Architecture?

Aktuelle Blog-Artikel

  1. Angular’s Future Without NgModules – Part 1: Lightweight Solutions Using Standalone Components
  2. Angular’s Future Without NgModules – Part 2: What Does That Mean for Our Architecture?

Nur einen Schritt entfernt!

Stellen Sie noch heute Ihre Anfrage,
wir beraten Sie gerne!

Jetzt anfragen!