Angular Standalone Components Unleashed: Exploring the Magic of a World Without NgModule

Angular Aug 4, 2023

In the world of software engineering, our primary goal is to deliver fast and efficient web applications that cater to the needs of a large number of users. However, achieving this objective can be a daunting task, especially when dealing with the complexities of a framework like Angular. Thankfully, standalone components offer a solution to these challenges.

At Halodoc, we have acknowledged the significance of optimising for efficiency and scalability to provide exceptional user experiences. This led us to explore the transformative power of standalone components within the Angular framework. By adopting a modular and self-contained approach, standalone components have empowered us to streamline our development processes and enhance code modularity, resulting in faster and more efficient web applications.

In this blog, we will explore the transformative power of standalone components in the context of delivering high-performance web applications for a large user base.

What are standalone components?

In Angular, a standalone component refers to a self-contained and reusable unit of code that encapsulates logic, data, and UI elements. Unlike regular components, standalone components are not dependent on Angular's NgModule system for their configuration and dependencies. By using standalone components, developers have more flexibility and control over their codebase. They can create small, focused components that are easier to understand, test, and maintain. Standalone components also promote better organisation of code, as they can be grouped based on their functionality or purpose.

Additionally, standalone components lead to enhanced efficiency and reduced bundle size. Since they are self-contained units, only the necessary dependencies are included in the final build, resulting in a more optimised application.

Unveiling the need: Why standalone components enter the picture in Angular development?

In Angular, it is common practice to import components and services within a module to ensure their availability throughout the module's components . However, this approach can lead to unnecessary dependencies and an increase in bundle size, especially when certain components do not necessitate utilisation of all imported components and services.

This is where standalone components come into the picture as a solution. Standalone components provide a more granular and focused approach to module development in Angular. Rather than importing an entire module with potentially unused components and services, standalone components allow you to create individual components that encapsulate their own functionality and dependencies.

By integrating independent modules, you gain the flexibility to selectively import the specific components and services required by each module individually. This approach eliminates the inclusion of redundant code, leading to a more streamlined application with optimised efficiency.

The beauty of standalone components lies in their modular and self-contained nature. Each standalone component is developed independently, with its own set of dependencies and functionality. This isolation allows for better code organisation and maintainability, as each component only has access to the dependencies it explicitly requires.

In the case of the Artikel Search Result Component, which does not need components A and C but only requires component B, using a standalone component approach ensures that only the necessary code related to component B is included in the bundle. This optimisation results in a more efficient application, with a streamlined bundle and enhanced overall functioning.

By leveraging standalone components, you can achieve a fine-grained control over the dependencies of your components, resulting in a more optimised and streamlined Angular application. It enables you to eliminate unused code, and enhance the maintainability of your application by promoting modularity and isolation.

Unleashing component-level control: How standalone components optimise bundle size?

By transforming individual components into standalone components, a remarkable shift occurs in the Angular application structure. Each component becomes self-sufficient, no longer reliant on external modules for its dependencies. This newfound independence brings forth numerous benefits, one of which is the elimination of unnecessary imports and unused dependencies.

Consider the example of the ArtikelModule. Previously, components below the ArtikelModule would need to import the entire module to access its functionalities. However, with the introduction of standalone components, this module can be removed altogether. Each component can now import its required dependencies directly, without the need for an intermediary module.

As a result, the application becomes more streamlined and efficient. Unused dependencies are omitted, leading to a concise, maintainable, and optimized codebase, enhancing overall functionality. By selectively importing only the necessary components and services, the application achieves a higher level of efficiency and organisation.

This modular restructuring ensures that each component has its own set of dependencies, tailored to its specific requirements. Components can now be loaded directly from the app modules, without the need to import unnecessary modules such as ArtikelModule in the above example.

Through the adoption of standalone components, the Angular application gains enhanced modularity, increased code organisation, and improved maintainability. By eliminating unused dependencies, this architectural approach empowers developers to build more focused and efficient applications.

Standalone Component Generation:

  • Run the following command in terminal to generate standalone component
The command to generate standalone component 

A Closer Look at their Unique Structure and Configuration:

standalone component

                   

In the aforementioned example, we can observe a distinct trait that sets standalone components apart. By examining the properties of standalone: true and imports: [], we can confidently identify a particular component as a standalone component.

The standalone: true property signifies that the component operates independently and does not rely on other modules or components within the application. This encapsulation allows for greater modularity, as the standalone component functions autonomously, separate from any external dependencies.

Furthermore, the presence of imports: [] indicates that the standalone component does not require any specific module imports. Unlike regular Angular components that often rely on imported modules for functionality and dependencies, standalone components are self-contained entities that import only the necessary resources directly.

Migrating to standalone components

Before migrating  to standalone components, it's important to assess your project and its dependencies. Identify the specific components  that heavily rely on other dependency . This evaluation will help you plan the migration strategy and determine the effort required to update your codebase.

Critical factors to address before adopting a standalone component:

Before using the schematic, please ensure that the project:

  1. Is using Angular 15.2.0 or later.(Important Note: Although the "standalone" feature is available in Angular 14, it is worth mentioning that it is not yet considered stable. Consequently, it is generally recommended to avoid utilising it within Angular 14.)
  2. Builds without any compilation errors.
  3. Is on a clean Git branch and all work is saved.

Follow the below steps to migrate a component to standalone component:

Migrations steps:

To ensure a smooth migration process, follow the steps below in the specified order, verifying the build and functionality of your code after each step:

1. Convert declarations to standalone:

Run the command ng g @angular/core:standalone and choose the option "Convert all components, directives, and pipes to standalone." Validate that your code builds and runs as expected.

In this mode, the migration converts all components, directives and pipes to standalone by setting standalone: true and adding dependencies to their imports array.

Before:

// shared.module.ts
@NgModule({
  imports: [CommonModule],
  declarations: [GreeterComponent],
  exports: [GreeterComponent]
})
export class SharedModule {}
// greeter.component.ts
@Component({
  selector: 'greeter',
  template: '<div *ngIf="showGreeting">Hello</div>',
})
export class GreeterComponent {
  showGreeting = true;
}

After:

// shared.module.ts
@NgModule({
  imports: [CommonModule, GreeterComponent],
  exports: [GreeterComponent]
})
export class SharedModule {}
// greeter.component.ts
@Component({
  selector: 'greeter',
  template: '<div *ngIf="showGreeting">Hello</div>',
  standalone: true,
  imports: [StandaloneComponents]
})
export class GreeterComponent {
  showGreeting = true;
}

2. Remove unnecessary NgModules:

Run the command ng g @angular/core:standalone and choose the option "Remove unnecessary NgModule classes." Again, verify that your code builds and functions correctly.

Once the conversion of all declarations to standalone components is completed, the migration process can safely remove numerous NgModules. During this step, the migration automatically deletes module declarations and attempts to remove associated references.

If the migration is unable to delete a reference automatically, it leaves a TODO comment for manual removal.

/* TODO(standalone-migration): clean up removed NgModule reference manually */

The following criteria determine if a module can be safely removed:

  • No declarations: The module does not contain any declaration statements for components, directives, or pipes.
  • No providers: The module does not provide any services or dependencies.
  • No bootstrap components: The module does not have any components specified for bootstrapping.
  • No imports referencing ModuleWithProviders or unremovable modules: The module does not import any symbols related to ModuleWithProviders, nor does it reference any modules that cannot be removed.
  • No class members: The module's class does not contain any additional members. Empty constructors are ignored in this assessment.

Before:

// importer.module.ts
@NgModule({
  imports: [FooComponent, BarPipe],
  exports: [FooComponent, BarPipe]
})
export class ImporterModule {}

After:

// importer.module.ts
// Does not exist!

3.Switch to standalone bootstrapping API :

Run the command ng g @angular/core:standalone and select "Bootstrap the project using standalone APIs." Confirm that your code builds and behaves as intended after this step.

This step converts any usages of bootstrapModule to the new, standalone-based bootstrapApplication. It also switches the root component to standalone: true and deletes the root NgModule. If the root module has any providers or imports, the migration attempts to copy as much of this configuration as possible into the new bootstrap call.

Before:

// ./app/app.module.ts
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}
// ./app/app.component.ts
@Component({ selector: 'app', template: 'hello' })
export class AppComponent {}
// ./main.ts
import { platformBrowser } from '@angular/platform-browser';
import { AppModule } from './app/app.module';

platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e));

After:

// ./app/app.module.ts
// Does not exist!
// ./app/app.component.ts
@Component({ selector: 'app', template: 'hello', standalone: true })
export class AppComponent {}
// ./main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent).catch(e => console.error(e));

After completing the aforementioned step, proceed with linting and formatting checks. If any issues are identified, address them appropriately to ensure the resulting code successfully passes the checks.

Improving component modularity: pre and post migration metrics:

In an effort to enhance component modularity and promote reusability, we embarked on the task of migrating an existing component to a standalone module in Halodoc. This migration involved isolating the component's functionality, dependencies, and resources, ensuring it can function independently.

To assess the effectiveness of the migration, we carefully measured various metrics both before and after the transition.

Before:

Before migration metrics

After:

After migration metrics

By comparing the metrics before and after the migration, we gained valuable insights into the impact of this transformation on the component's efficiency, maintainability, and overall system performance.

In the process of migrating components to standalone modules, we have encountered common challenges and limitations. Few of them are discussed below:

  • Cannot delete all of the .modules: In standalone component migration, it is not always possible to delete all module files immediately. When a module imports another module that uses .forRoot() or .forChild(), deleting the module file will cause errors. To handle this situation, the module can be kept, but only the .forRoot() or .forChild() calls should remain like this
TranslateModule.forChild({
      loader: {
        provide: TranslateLoader,
        useFactory: translateLoaderFactory,
        deps: [TransferState, HttpClient, UtilitiesService, PLATFORM_ID],
      },
      isolate: true,
    }),

except for RouterModule.forRoot() because it’s already handled by Angular.

  • Not every module can do the loadComponent: For instance, let's consider a module with only one page. In such cases, splitting the bundle size using loadComponent requires at least two pages. When a module consists of a single page, the focus shifts to splitting the dependencies between components rather than splitting the module itself.
  • Code that cannot be statically analysed:  The migration schematic uses static analysis to understand the code and determine where to make changes. The migration may skip any classes with metadata that cannot be statically analysed at build time.
  • Given the scale and intricacy of the migration process, there are certain scenarios that the schematic may not be able to handle:
  1. Custom wrappers around Angular APIs: The schematic relies on direct calls to Angular APIs for analysis and modification. However, it may not recognise custom wrappers that encapsulate Angular APIs. For instance, if you define a custom function like customConfigureTestModule that wraps TestBed.configureTestingModule, components declared within it may not be recognised by the schematic.
  2. Unit test imports: Since unit tests are not compiled ahead-of-time (AoT), the schematic might encounter challenges in accurately adding imports to components within unit tests. This could result in imports not being entirely correct due to the limitations of the AoT compilation process for unit tests.

Embracing the inevitable challenges, we are steadfast in our determination to overcome them and achieve a successful implementation.

Conclusion

The adoption of standalone components in Angular brings significant benefits to the development process. By isolating components into standalone modules, we achieve improved modularity, reusability, and maintainability. The process of migrating to standalone components may present challenges. However, by carefully addressing the challenges and leveraging effective strategies, we can successfully overcome them. Standalone components allow us to create more modular and cohesive codebases, making it easier to manage and scale our Angular applications. They encourage code reuse, simplify maintenance and testing efforts, and contribute to an overall improved user experience. By embracing the concept of standalone components and investing the necessary time and effort in the migration process, we can unlock the potential to build more robust, efficient, and maintainable Angular applications. This approach empowers us to deliver high-quality software solutions that can adapt to changing requirements and ensure a positive user experience.

References:

Join us

Scalability, reliability and maintainability are the three pillars that govern what we build at Halodoc Tech. We are actively looking for engineers at all levels and  if solving hard problems with challenging requirements is your forte, please reach out to us with your resumé at careers.india@halodoc.com.


About Halodoc

Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 3500+ pharmacies in 100+ cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allow patients to book a doctor appointment inside our application. We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek, Astra, Temasek and many more. We recently closed our Series C round and In total have raised around USD$180 million for our mission. Our team works tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.

Maheli Dutta

Web SDE - Passionate about crafting user-friendly web solutions. Driven to deliver exceptional web experiences.