Standalone vs NgModules
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.
The Legacy Module-Based Architecture
Section titled “The Legacy Module-Based Architecture”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
SharedModulethat re-exported them. BrowserModulewas imported once in the root module;CommonModulein 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() ]};Side-by-Side Comparison
Section titled “Side-by-Side Comparison”| Aspect | NgModules (Legacy) | Standalone (Modern) |
|---|---|---|
| Component declaration | Must be declared in a module’s declarations array | Self-contained — no module needed |
| Dependency management | Import modules that export the needed directives/pipes | Import dependencies directly in the component’s imports array |
| Bootstrapping | platformBrowserDynamic().bootstrapModule(AppModule) | bootstrapApplication(AppComponent, appConfig) |
| Lazy loading | Requires a dedicated NgModule per lazy route | Lazy-load individual components directly with loadComponent |
| Shared components | Need a SharedModule to group and re-export | Import each component directly where needed |
| Boilerplate | High — module files, declarations, imports, exports | Minimal — components are self-describing |
| Tree-shaking | Modules can prevent effective tree-shaking | Better tree-shaking — only used components are bundled |
| Default since | Angular 2 | Angular 19 (available since Angular 14) |
Lazy Loading — Modules vs Standalone
Section titled “Lazy Loading — Modules vs Standalone”Legacy (module-based):
const routes: Routes = [ { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module') .then(m => m.DashboardModule) }];Modern (standalone):
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) }];When Are Modules Still Useful?
Section titled “When Are Modules Still Useful?”Modules haven’t disappeared — they remain useful in specific scenarios:
- Third-party libraries — Many libraries still expose
NgModuleclasses (e.g.,MatButtonModule). You can import them directly in standalone componentimports. - 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.
Mixing Both Approaches
Section titled “Mixing Both Approaches”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 {}Migration Strategy
Section titled “Migration Strategy”If you’re working on an existing module-based project, consider migrating incrementally:
- New components — Always create as standalone (default in Angular 18+).
- Leaf components — Migrate components that have no dependents first.
- Shared components — Convert shared components to standalone and remove them from
SharedModuledeclarations. - Feature modules — Replace
loadChildrenwithloadComponentroute by route. - Root module — Finally, replace
AppModulewithbootstrapApplicationandappConfig.
Angular provides a schematic to assist with migration:
Terminal window ng generate @angular/core:standalone