AngularJSStandalone Components

Standalone Components

Standalone components are the modern way to write Angular code. Introduced as a developer preview in Angular 14 and made stable in Angular 15, they became the default in Angular 17.

Standalone components eliminate the need for NgModule by allowing components, directives, and pipes to declare their own dependencies directly. The result is simpler, more readable code with less boilerplate.

What Problem Do Standalone Components Solve?

In the NgModule-based approach, every component had to:

  1. Be declared in exactly one NgModule
  2. Import NgModules to access other components, directives, and pipes
  3. Export components that other modules needed to use

This created a lot of boilerplate, and tracking "which module owns what" was a constant source of confusion — especially for beginners.

Standalone components take a simpler approach: each component declares exactly what it needs.

NgModule vs Standalone — Side by Side

TS
// ===== OLD: NgModule approach =====
// 1. product-card.component.ts
@Component({
  selector: 'app-product-card',
  templateUrl: './product-card.component.html',
})
export class ProductCardComponent { }

// 2. products.module.ts (required boilerplate)
@NgModule({
  declarations: [ProductCardComponent, ProductListComponent],
  imports: [CommonModule, RouterModule],
  exports: [ProductCardComponent],
})
export class ProductsModule { }

// 3. app.module.ts (must import ProductsModule to use ProductCardComponent)
@NgModule({
  imports: [BrowserModule, ProductsModule],
  bootstrap: [AppComponent],
})
export class AppModule { }

TS
// ===== NEW: Standalone approach =====
// 1. product-card.component.ts (self-contained, no module needed)
@Component({
  selector: 'app-product-card',
  standalone: true,                 // <-- the key flag
  imports: [RouterLink, CurrencyPipe],  // declare dependencies directly
  templateUrl: './product-card.component.html',
})
export class ProductCardComponent { }

// 2. product-list.component.ts (import ProductCardComponent directly)
@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [ProductCardComponent, NgFor],  // direct import — no module!
  templateUrl: './product-list.component.html',
})
export class ProductListComponent { }

// 3. main.ts (no AppModule, just bootstrap)
bootstrapApplication(AppComponent, appConfig);
Creating a Standalone Component

Any component with standalone: true in its decorator is a standalone component. This is the default since Angular 17:

Bash
# Angular 17+ generates standalone by default
ng generate component user-profile

# Explicitly standalone (if needed)
ng generate component user-profile --standalone

# If you need NgModule-based (legacy projects)
ng generate component user-profile --no-standalone

TS
import { Component, Input, signal } from '@angular/core';
import { DatePipe, NgClass } from '@angular/common';
import { RouterLink } from '@angular/router';

interface User {
  id: number;
  name: string;
  email: string;
  joinDate: Date;
  isActive: boolean;
}

@Component({
  selector: 'app-user-profile',
  standalone: true,
  imports: [
    DatePipe,       // for {{ date | date }}
    NgClass,        // for [ngClass]
    RouterLink,     // for routerLink directive
  ],
  template: `
    <div [ngClass]="{ active: user.isActive, inactive: !user.isActive }">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
      <p>Member since {{ user.joinDate | date:'longDate' }}</p>
      <a [routerLink]="['/users', user.id]">View Profile</a>
    </div>
  `,
})
export class UserProfileComponent {
  @Input({ required: true }) user!: User;
}
Bootstrapping a Standalone App

A standalone application does not use AppModule. Instead, main.ts calls bootstrapApplication() with the root component and an optional configuration:

TS
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

TS
// app.config.ts — application-level providers
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { routes } from './app.routes';
import { authInterceptor } from './core/interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor])),
    provideAnimationsAsync(),
  ],
};
Note
The appConfig / ApplicationConfig pattern replaces the AppModule's providers and imports arrays. Each provide* function is a tree-shakable alternative to importing a module.
The imports Array — What Can Go In It

A standalone component's imports array can contain:

Import Type

Example

What It Provides

Standalone component

UserCardComponent

Use <app-user-card> in template

Standalone directive

NgClass, NgStyle, RouterLink

Use directives in template

Standalone pipe

DatePipe, CurrencyPipe, AsyncPipe

Use | date, | currency, etc.

NgModule (legacy)

ReactiveFormsModule, HttpClientModule

Access everything the module exports

CommonModule

CommonModule

NgIf, NgFor, pipes (use individual imports instead)

TS
@Component({
  selector: 'app-order-form',
  standalone: true,
  imports: [
    // Individual imports (preferred — better tree-shaking)
    NgIf,
    NgFor,
    DatePipe,
    CurrencyPipe,
    AsyncPipe,
    RouterLink,
    // Reactive Forms module
    ReactiveFormsModule,
    // Custom components
    ProductSelectorComponent,
    AddressFormComponent,
    // Legacy NgModule — sometimes unavoidable for 3rd party libs
    SomeThirdPartyModule,
  ],
  templateUrl: './order-form.component.html',
})
export class OrderFormComponent { }
Tip
Prefer importing individual pieces (NgIf, NgFor, DatePipe) over CommonModule. The Angular compiler tree-shakes unused code, so importing only what you need produces smaller bundles.
Lazy Loading Standalone Components

One of the major benefits of standalone components is simpler lazy loading. You can lazy-load a single component directly without a feature module:

TS
// app.routes.ts — lazy load individual components
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full',
  },
  {
    path: 'home',
    loadComponent: () =>
      import('./features/home/home.component').then(m => m.HomeComponent),
  },
  {
    path: 'products',
    loadComponent: () =>
      import('./features/products/product-list.component')
        .then(m => m.ProductListComponent),
  },
  {
    path: 'products/:id',
    loadComponent: () =>
      import('./features/products/product-detail.component')
        .then(m => m.ProductDetailComponent),
  },
  // Lazy load a group of routes (like a feature module's routes)
  {
    path: 'admin',
    loadChildren: () =>
      import('./features/admin/admin.routes').then(m => m.adminRoutes),
    canActivate: [authGuard],
  },
];

TS
// features/admin/admin.routes.ts — child routes file
import { Routes } from '@angular/router';

export const adminRoutes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./dashboard/admin-dashboard.component')
        .then(m => m.AdminDashboardComponent),
  },
  {
    path: 'users',
    loadComponent: () =>
      import('./users/user-management.component')
        .then(m => m.UserManagementComponent),
  },
];
Standalone Directives and Pipes

The standalone: true flag works for directives and pipes too, not just components:

TS
// Standalone directive
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true,    // can be imported directly by any component
})
export class HighlightDirective {
  @Input() appHighlight = 'yellow';

  constructor(private el: ElementRef) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.el.nativeElement.style.backgroundColor = this.appHighlight;
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

// Standalone pipe
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate',
  standalone: true,    // can be imported directly
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, maxLength = 50): string {
    if (value.length <= maxLength) return value;
    return value.slice(0, maxLength) + '...';
  }
}

TS
// Using standalone directive and pipe in a component
@Component({
  selector: 'app-article',
  standalone: true,
  imports: [HighlightDirective, TruncatePipe],  // direct import
  template: `
    <p [appHighlight]="'#ffeb3b'">Hover me!</p>
    <p>{{ article.content | truncate:200 }}</p>
  `,
})
export class ArticleComponent { }
Migrating from NgModule to Standalone

Angular provides an automated migration schematic:

Bash
# Automatically migrate components to standalone
ng generate @angular/core:standalone

# This will ask which migration to run:
# 1. Convert all components, directives and pipes to standalone
# 2. Remove unnecessary NgModules
# 3. Bootstrap the project using standalone APIs
? Choose the type of migration:
❯ Convert all components, directives and pipes to standalone
  Remove unnecessary NgModules
  Bootstrap the application using standalone APIs

Run all three in sequence. Each step is incremental and safe to review before committing.

Warning
Run the migration on a clean git branch. Review the generated changes carefully — the automated migration handles most cases but may need manual adjustments for complex setups (e.g., shared module patterns, complex provider configs).
Standalone vs NgModule — When to Use Each

Scenario

Recommendation

New Angular project

Use standalone (the default in Angular 17+)

Existing NgModule project

Migrate gradually with ng generate @angular/core:standalone

Third-party library integration

Import the library module into standalone component imports

Large team with many shared components

Standalone with feature-based route files

Legacy project on Angular 12 or earlier

Keep NgModules until upgrading Angular

Summary — Key Points
  1. Add standalone: true to @Component, @Directive, or @Pipe to make it standalone

  2. Standalone components declare their own dependencies in the imports array

  3. Bootstrap with bootstrapApplication() instead of platformBrowserDynamic().bootstrapModule()

  4. Use appConfig (ApplicationConfig) with provide*() functions instead of AppModule providers

  5. Lazy load individual components with loadComponent: () => import(...).then(m => m.XComponent)

  6. Prefer importing individual directives/pipes over CommonModule for smaller bundles

  7. Run ng generate @angular/core:standalone to migrate existing projects automatically