Angular - Standalone Components vs Modules

Josh Dunn

an overview of the difference in Angular Standalone Components and Modules

hero background

Angular stands as one of the premier TypeScript frameworks/libraries for web application development, empowering developers to create sophisticated applications with ease. In this post, we delve into the distinctions between standalone components and modules in Angular.

Angular Overview

Angular is a TypeScript-based, free and open-source single-page web application framework led by the Angular Team at Google and by a community of individuals and corporations. Angular streamlines web development by abstracting low-level JavaScript intricacies, leveraging TypeScript for type-safe component validation. It offers flexibility akin to other popular frameworks like React and Vue, facilitating the creation of diverse applications.

What is a module?

Angular defines modules as containers for a cohesive block of code dedicated to an application domain, a workflow, or a closely related set of capabilities. They can contain components, service providers, and other code files whose scope is defined by the containing module. They can import functionality that is exported from other modules, and export selected functionality for use by other modules.

A basic example of a module is defined below

@NgModule({
  imports: [BrowserModule, RouterModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
})
export class AppModule {}

Standalone Components vs. Modules:

Traditionally, Angular necessitated defining components within modules. However, the introduction of standalone components in Angular 15 revolutionised this approach. Standalone components operate autonomously, eliminating the requirement for modules. Now, components solely necessitate the essential inputs to function. Under the hood there is a number of changes that occur to enable standalone components to function, however for developers there is only a small code change that needs to be made.

Using Modules

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {}

Using Standalone Components

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {}

There are two main differences that you will notice, the first being the 'standalone' flag which is all Angular needs to mark a component as a standalone components. The other being the 'imports' array which is used to define the modules, services, pipes or components that the component will use. Traditionally this was done through the module but when using a standalone component there is no module, so all dependencies have to be defined at the component level.

Why use standalone components?

The adoption of standalone components in Angular brings forth several advantages:

Reduced bundle size and improved build times
As shown in the examples below, the initial bundle size is noticeably smaller even for a scaffolded application which only has a single component.

Using modules

$ npm run build

> module-test@0.0.0 build
> ng build

⠧ Building...
Initial chunk files   | Names         |  Raw size | Estimated transfer size
main-NKGJUDBB.js      | main          | 185.38 kB |                49.39 kB
polyfills-RT5I6R6G.js | polyfills     |  33.10 kB |                10.72 kB
styles-5INURTSO.css   | styles        |   0 bytes |                 0 bytes

                      | Initial total | 218.48 kB |                60.11 kB
                      
Application bundle generation complete. [6.872 seconds]

Using standalone components

$ npm run build

> standalone-test@0.0.0 build
> ng build

⠙ Building...
Initial chunk files   | Names         |  Raw size | Estimated transfer size
chunk-UMNEWAQH.js     | -             |  94.34 kB |                28.04 kB
main-MUQYWPEY.js      | main          |  76.15 kB |                19.16 kB
polyfills-RT5I6R6G.js | polyfills     |  33.10 kB |                10.72 kB
styles-5INURTSO.css   | styles        |   0 bytes |                 0 bytes

                      | Initial total | 203.59 kB |                57.92 kB

Lazy chunk files      | Names         |  Raw size | Estimated transfer size
chunk-DJ7SN3S3.js     | browser       |  65.75 kB |                17.09 kB

Application bundle generation complete. [4.611 seconds]

Enhanced lazy loading and routing capabilities

Previously lazy loading would load an entire module the first time a route was traversed and depending on the module size this could provide the user a noticeable load time. With the introduction of standalone components, lazy loading has been improved to allow for any route to lazily load its routed, standalone component by using 'loadComponent' :

export const ROUTES: Route[] = [
  {path: 'foo', loadComponent: () => import('./foo/bar.component').then(mod => mod.BarComponent)},
];

We can then take this a step further to load multiple standalone components when a route is traversed using loadChildren:

export const routes: Routes = [
    {
        path: '',
        component: HomeComponent,
    },
    {
        path: 'first-route',
        loadChildren: () => import('./first-route/routes'),
    },
    {
        path: 'second-route',
        loadChildren: () => import('./second-route/routes'),
    },
    { path: '**', component: NotFoundComponent },
];

Where the routes file takes advantage of the export default syntax as the router understands and automatically unwraps import() calls with default exports:

export default [
    {
        path: '',
        component: FirstRouteComponent,
        children: [
            {
                path: '',
                component: SubComponent,
            },
            {
                path: 'details/:fooID',
                component: AnotherSubComponent,
            },
        ],
    },
] satisfies Route[];

Migration and Implementation

Angular facilitates seamless migration to standalone components through its schematics. The complexity of migration varies depending on the size and structure of the application. Standalone components promote modularity and adhere to the single responsibility principle, enhancing code maintainability.

Future Outlook

Standalone components and modules will coexist within the Angular ecosystem for the foreseeable future. While there are no immediate plans to discontinue support for modules, transitioning to standalone components is recommended for long-term maintenance and performance optimization.

Wrapping up

Understanding the nuances between standalone components and modules in Angular is crucial for maximizing development efficiency and application performance. By embracing standalone components, developers can streamline their workflows, enhance application performance, and future-proof their projects for continued success in the evolving landscape of web development.

REFERENCES

https://angular.io/guide/architecture-modules#introduction-to-modules