Ir al contenido

Standalone vs NgModules

Esta página aún no está disponible en tu idioma.

Standalone Components vs NgModules — The Architecture Shift

Section titled “Standalone Components vs NgModules — The Architecture Shift”

Angular has undergone a major architectural shift. Traditionally, apps were built entirely with NgModules, where every component, directive, and pipe had to be declared inside a module. While this ensured consistency, it also added a lot of boilerplate.

Since Angular 14, standalone components have simplified this model. From Angular 19 onward, they are the default way to create components — you no longer need to specify standalone: true.


In the traditional approach, the application was organized through @NgModule classes:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { UserCardComponent } from './user-card/user-card.component';
import { SharedModule } from './shared/shared.module';
@NgModule({
declarations: [AppComponent, UserCardComponent], // Register components here
imports: [BrowserModule, SharedModule], // Import other modules
exports: [], // Expose to other modules
bootstrap: [AppComponent] // Root component
})
export class AppModule {}

Bootstrapping with modules:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

Key characteristics of the module-based approach:

  • Every component must be declared in exactly one module.
  • Shared components required a SharedModule that re-exported them.
  • BrowserModule was imported once in the root module; CommonModule in feature modules.
  • Lazy loading required a dedicated module per route.
  • Added significant boilerplate for small features.

The Modern Standalone Architecture (Angular 18+)

Section titled “The Modern Standalone Architecture (Angular 18+)”

With standalone components, each component is self-contained — it imports what it needs directly:

import { Component, input } from '@angular/core';
import { DatePipe } from '@angular/common';
@Component({
selector: 'app-user-card',
imports: [DatePipe],
template: `
<div class="card">
<h3>{{ name() }}</h3>
<p>Member since: {{ joinDate() | date:'mediumDate' }}</p>
</div>
`
})
export class UserCardComponent {
name = input.required<string>();
joinDate = input.required<Date>();
}

Bootstrapping without modules:

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig);
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app/app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient()
]
};

AspectNgModules (Legacy)Standalone (Modern)
Component declarationMust be declared in a module’s declarations arraySelf-contained — no module needed
Dependency managementImport modules that export the needed directives/pipesImport dependencies directly in the component’s imports array
BootstrappingplatformBrowserDynamic().bootstrapModule(AppModule)bootstrapApplication(AppComponent, appConfig)
Lazy loadingRequires a dedicated NgModule per lazy routeLazy-load individual components directly with loadComponent
Shared componentsNeed a SharedModule to group and re-exportImport each component directly where needed
BoilerplateHigh — module files, declarations, imports, exportsMinimal — components are self-describing
Tree-shakingModules can prevent effective tree-shakingBetter tree-shaking — only used components are bundled
Default sinceAngular 2Angular 19 (available since Angular 14)

Legacy (module-based):

app-routing.module.ts
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module')
.then(m => m.DashboardModule)
}
];

Modern (standalone):

app.routes.ts
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
.then(c => c.DashboardComponent)
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes')
.then(r => r.ADMIN_ROUTES)
}
];

Modules haven’t disappeared — they remain useful in specific scenarios:

  • Third-party libraries — Many libraries still expose NgModule classes (e.g., MatButtonModule). You can import them directly in standalone component imports.
  • Large shared feature groups — If you have 20+ tightly related components that always go together, a module can simplify imports.
  • Legacy codebases — Migrating gradually is perfectly valid; standalone and module-based components can coexist.

Angular allows seamless interoperability between standalone and module-based components:

@Component({
selector: 'app-dashboard',
imports: [MatCardModule, MatButtonModule, UserCardComponent],
template: `
<mat-card>
<app-user-card [name]="'Alice'" [joinDate]="today" />
<button mat-button>Action</button>
</mat-card>
`
})
export class DashboardComponent {
today = new Date();
}
@NgModule({
declarations: [LegacyWidgetComponent],
imports: [CommonModule, UserCardComponent],
exports: [LegacyWidgetComponent]
})
export class WidgetsModule {}

If you’re working on an existing module-based project, consider migrating incrementally:

  1. New components — Always create as standalone (default in Angular 18+).
  2. Leaf components — Migrate components that have no dependents first.
  3. Shared components — Convert shared components to standalone and remove them from SharedModule declarations.
  4. Feature modules — Replace loadChildren with loadComponent route by route.
  5. Root module — Finally, replace AppModule with bootstrapApplication and appConfig.

Angular provides a schematic to assist with migration:

Terminal window
ng generate @angular/core:standalone