angular.courses

From NgModules to Standalone Components

The shift from module-based to standalone component architecture

Version status:Preview in v14Stable in v15

CLI Migration Script

$ ng generate @angular/core:standalone

Overview

Standalone components represent one of the most significant architectural changes in Angular's history. They eliminate the need for NgModules in most cases, making Angular applications simpler to write, understand, and maintain.

The Evolution

v2NgModule-based Architecture

All components required declaration in an NgModule.

v14previewStandalone Components

Standalone components introduced as opt-in feature.

v15stableStandalone

Standalone becomes stable and recommended for new projects.

v19Standalone Default

standalone: true is now the default for new components.

Code Comparison

Basic Component

NgModule-basedBefore v13
greeting.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `<h1>Hello, {{ name }}!</h1>`
})
export class GreetingComponent {
name = 'World';
}
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { GreetingComponent } from './greeting.component';
@NgModule({
declarations: [GreetingComponent],
imports: [BrowserModule],
bootstrap: [GreetingComponent]
})
export class AppModule {}
Standalonev14+
greeting.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-greeting',
standalone: true,
template: `<h1>Hello, {{ name }}!</h1>`
})
export class GreetingComponent {
name = 'World';
}
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { GreetingComponent } from './greeting.component';
bootstrapApplication(GreetingComponent);

Using Other Components

Import via ModuleBefore v13
user-card.component.ts
@Component({
selector: 'app-user-card',
template: `<div>{{ user.name }}</div>`
})
export class UserCardComponent {
@Input() user!: User;
}
// user-list.component.ts
@Component({
selector: 'app-user-list',
template: `
<app-user-card
*ngFor="let user of users"
[user]="user">
</app-user-card>
`
})
export class UserListComponent {
users: User[] = [];
}
// users.module.ts
@NgModule({
declarations: [UserCardComponent, UserListComponent],
imports: [CommonModule],
exports: [UserListComponent]
})
export class UsersModule {}
Direct Importsv14+
user-card.component.ts
@Component({
selector: 'app-user-card',
standalone: true,
template: `<div>{{ user.name }}</div>`
})
export class UserCardComponent {
user = input.required<User>();
}
// user-list.component.ts
@Component({
selector: 'app-user-list',
standalone: true,
imports: [UserCardComponent],
template: `
@for (user of users; track user.id) {
<app-user-card [user]="user" />
}
`
})
export class UserListComponent {
users: User[] = [];
}

Application Bootstrap

Module BootstrapBefore v13
main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
// app.module.ts
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule,
RouterModule.forRoot(routes)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
Standalone Bootstrapv14+
main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
provideRouter(routes)
]
});

Lazy Loading

Lazy ModuleBefore v13
app-routing.module.ts
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module')
.then(m => m.AdminModule)
}
];
// admin.module.ts
@NgModule({
declarations: [AdminComponent, DashboardComponent],
imports: [
CommonModule,
AdminRoutingModule
]
})
export class AdminModule {}
Lazy Componentv14+
app.routes.ts
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.component')
.then(c => c.AdminComponent)
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes')
.then(r => r.adminRoutes)
}
];

Key Differences

Architecture:NgModule requiredComponent-first
Dependencies:Module importsComponent imports
Bootstrap:bootstrapModule()bootstrapApplication()
Lazy Loading:loadChildren (module)loadComponent

Benefits

Developer Experience

Simpler mental model, no NgModule juggling

Bundle Size

Better tree shaking, import only what you use

Maintainability

Clearer dependencies per component

Testability

No module configuration in tests

Performance

Faster compilation, less metadata

Learning Curve

Easier for new developers

Benefits of Standalone

  1. Simpler Mental Model: No more wondering which module to put things in
  2. Better Tree Shaking: Only import what you use
  3. Faster Compilation: Less metadata to process
  4. Clearer Dependencies: Component imports show exactly what's needed
  5. Easier Testing: No module configuration required

Gradual Migration

You don't need to convert everything at once! Standalone and NgModule-based components can coexist. Standalone components can import modules, and modules can import standalone components.

Provider Configuration

With standalone, providers are configured at the application level using provide* functions:

bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptors([authInterceptor])),
provideAnimations(),
// Custom providers
{ provide: API_URL, useValue: 'https://api.example.com' }
]
});

Common Provider Functions

| Function | Replaces | |----------|----------| | provideRouter() | RouterModule.forRoot() | | provideHttpClient() | HttpClientModule | | provideAnimations() | BrowserAnimationsModule | | provideNoopAnimations() | NoopAnimationsModule |