<link rel="stylesheet" href="assets/fonts/inter.css"> Angular Standalone Components: The Complete Migration… | Viral Patel Studio Skip to main content

Angular Standalone Components: The Complete Migration Guide for 2025

May 28, 2025By Viral Patel

Migrate your Angular NgModule app to standalone components in 2025 with the schematic tool, lazy routes, testing fixes, and pitfalls to avoid.

Angular Standalone Components: The Complete Migration Guide for 2025

Every time you add a new feature to an Angular app, you reach for the same files: app.module.ts, perhaps shared.module.ts, maybe a feature module hiding somewhere in the project structure. You declare the component, import dependencies, export what's needed, and hope you didn't break a shared service. This repetitive overhead—the "NgModule tax"—is exactly what Angular standalone components eliminate. In 2025, the Angular team has made standalone the default path forward, and for good reason. This guide walks you through a complete, production-ready migration strategy that fits any app size, from small dashboards to enterprise codebases.


What Are Angular Standalone Components?

Standalone components represent a fundamental shift in how Angular organizes code. Instead of declaring components in an NgModule, a standalone component manages its own imports directly.

The core change is the standalone: true flag (which is implicit in Angular 17+) combined with an imports array that replaces module-level declarations:

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-user-profile',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <div>
      <h2>{{ username }}</h2>
      <input [(ngModel)]="bio" placeholder="Edit bio" />
    </div>
  `
})
export class UserProfileComponent {
  username = 'Alice';
  bio = '';
}

Standalone works for components, directives, and pipes. A standalone pipe or directive can be imported directly into any other standalone component without an intermediate NgModule wrapper. This flattens the dependency graph and eliminates entire categories of boilerplate.


Why Migrate in 2025?

The Angular team's stance is clear: NgModule is the legacy path. Three compelling reasons justify migration now:

1. Angular's Official Direction Standalone is the default in Angular 17+. The framework team allocates resources to standalone-first improvements, including hydration, deferred loading with @defer, and signal-based reactivity. New features are often designed around standalone first.

2. Tree-Shaking and Bundle Size With NgModules, imports leak into shared modules that pull in unused code. Standalone components create tight coupling between imports and usage, enabling the bundler to eliminate dead code. Real-world migrations have seen 15–25% bundle size reductions.

3. Developer Experience No more hunting through module files to understand what a component needs. Everything a component uses is declared right in the component. Adding a new feature no longer requires touches to multiple module files—reducing cognitive load and merge conflicts.


The Three-Pass Schematic Migration

Angular provides an official schematic to automate the conversion: ng generate @angular/core:standalone. Running this command executes three passes over your codebase.

Before Migration (NgModule-Based):

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { UserProfileComponent } from './user-profile.component';
import { FormsModule } from '@angular/forms';

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

// main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

After Migration (Standalone):

// app.config.ts
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient()
  ]
};

// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { UserProfileComponent } from './user-profile.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, UserProfileComponent],
  template: `<router-outlet />`
})
export class AppComponent {}

// 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));

Run the schematic with:

ng generate @angular/core:standalone

The tool prompts you to confirm changes and handles the three passes automatically:

  • Pass 1: Convert components, directives, and pipes to standalone.
  • Pass 2: Remove empty NgModules.
  • Pass 3: Update main.ts to use bootstrapApplication.

For large apps, you can run the schematic with --path src/app/my-feature to migrate one feature at a time.


bootstrapApplication and Application Config

The new bootstrap model replaces NgModule providers with a functional configuration object. The appConfig pattern centralizes dependency injection setup.

Complete app.config.ts Example:

import { ApplicationConfig, importProvidersFrom } 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 './auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(
      withInterceptors([authInterceptor])
    ),
    provideAnimationsAsync(),
    // Custom providers
    {
      provide: 'API_KEY',
      useValue: 'your-api-key-here'
    }
  ]
};

Pass appConfig to bootstrapApplication:

// 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));

The appConfig object is cleaner, testable, and aligns with Angular's modern direction. Notice that provideHttpClient is now a function, not an NgModule—this pattern replaces many old NgModule imports.


Migrating Lazy-Loaded Feature Modules

Lazy loading adapts seamlessly to standalone. Two patterns emerge:

1. Load a Single Component Route:

Use loadComponent to load a component on-demand:

// app.routes.ts
import { Routes } from '@angular/router';
import { AppComponent } from './app.component';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(m => m.DashboardComponent)
  },
  {
    path: 'settings',
    loadComponent: () => import('./settings/settings.component')
      .then(m => m.SettingsComponent)
  }
];

2. Load a Feature Module with Child Routes:

Use loadChildren with a standalone route array:

// app.routes.ts
export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES)
  }
];

// admin/admin.routes.ts
import { Routes } from '@angular/router';

export const ADMIN_ROUTES: Routes = [
  {
    path: 'users',
    loadComponent: () => import('./users/users.component')
      .then(m => m.UsersComponent)
  },
  {
    path: 'reports',
    loadComponent: () => import('./reports/reports.component')
      .then(m => m.ReportsComponent)
  }
];

Before (NgModule Lazy Loading):

// app.routes.ts (NgModule pattern)
const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module')
      .then(m => m.AdminModule)
  }
];

// admin/admin.module.ts
@NgModule({
  declarations: [UsersComponent, ReportsComponent],
  imports: [CommonModule, AdminRoutingModule]
})
export class AdminModule {}

The standalone approach eliminates the intermediate module file entirely. Routes become the dependency mechanism.


Shared Services and Dependency Injection

Standalone services follow the same patterns as NgModule services, but with clearer semantics.

Using providedIn Root:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  getUser() {
    return { id: 1, name: 'Alice' };
  }
}

providedIn: 'root' makes the service available app-wide without explicit module registration. This is the preferred pattern and works identically in standalone and NgModule apps.

Route-Level Providers:

Provide services only when a route is active:

// app.routes.ts
export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
    providers: [AdminService, RoleGuard]
  }
];

Using importProvidersFrom for NgModule Compatibility:

Some third-party libraries still export NgModules. The importProvidersFrom helper makes them work in standalone apps:

// app.config.ts
import { importProvidersFrom } from '@angular/core';
import { NgxMatDatetimeModule } from '@angular-material-components/datetime-picker';

export const appConfig: ApplicationConfig = {
  providers: [
    importProvidersFrom(NgxMatDatetimeModule),
    // ... other providers
  ]
};

Testing Standalone Components

Testing requires no NgModule setup. Use the TestBed.configureTestingModule with direct imports:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserProfileComponent } from './user-profile.component';
import { FormsModule } from '@angular/forms';

describe('UserProfileComponent', () => {
  let component: UserProfileComponent;
  let fixture: ComponentFixture<UserProfileComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserProfileComponent, FormsModule]
    }).compileComponents();

    fixture = TestBed.createComponent(UserProfileComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should update bio on input change', () => {
    component.bio = 'New bio';
    fixture.detectChanges();
    expect(component.bio).toBe('New bio');
  });
});

Notice the imports array directly in configureTestingModule. No declarations, no NgModule wrapper—just the dependencies the test needs. This makes tests cleaner and closer to how the component is actually used.


Common Migration Pitfalls

1. Circular Import Chains

Standalone apps can create circular dependencies if not carefully structured. Example:

// ❌ WRONG: Circular import
// parent.component.ts
import { ChildComponent } from './child.component';

// child.component.ts
import { ParentComponent } from './parent.component';

Solution: Extract shared logic into a service or use a barrel export that doesn't import both directions.

2. Missing CommonModule Imports

In NgModules, CommonModule was often imported globally. In standalone, each component must explicitly import directives it uses:

// ❌ WRONG: Forgot CommonModule
@Component({
  selector: 'app-list',
  standalone: true,
  imports: [], // Missing CommonModule
  template: `<div *ngIf="items.length">...</div>` // ngIf won't work!
})
export class ListComponent {}

// ✅ CORRECT: Import CommonModule or standalone directive
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-list',
  standalone: true,
  imports: [CommonModule],
  template: `<div *ngIf="items.length">...</div>`
})
export class ListComponent {}

// OR use standalone import:
import { NgIf } from '@angular/common';

@Component({
  selector: 'app-list',
  standalone: true,
  imports: [NgIf],
  template: `<div *ngIf="items.length">...</div>`
})
export class ListComponent {}

3. Third-Party Libraries Still Using NgModule

Some libraries haven't migrated. The schematic can't convert them. Use importProvidersFrom to wrap them or request standalone versions from maintainers.

4. Router Outlet in Root App Shell

Standalone apps still need RouterOutlet in the root component:

// ✅ CORRECT: Root component with RouterOutlet
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  template: `<router-outlet />`
})
export class AppComponent {}

Design System Components: Standalone Migration Checklist

If you're building a design system or component library, standalone is especially valuable. Consumers no longer need to import your NgModule—they import components directly.

Consider these internal resources for design system best practices:

  • [INTERNAL LINK: /blogs/step-by-step-guide-to-building-a-design-system-in-angular "Step-by-Step Guide to Building a Design System in Angular"]
  • [INTERNAL LINK: /blogs/typescript-strict-mode-angular-guide-2025 "TypeScript Strict Mode Angular Guide 2025"]

Migration Checklist for Design Systems:

  • Convert all components to standalone
  • Verify all exports work as standalone imports
  • Update library docs with standalone examples
  • Test component tree isolation (no implicit shared state)
  • Run bundle analysis to confirm tree-shaking improvements
  • Update peer dependency docs

Incremental Migration Strategy for Large Apps

For enterprise apps, migrating everything at once risks regressions. A feature-by-feature approach is safer.

Phase 1: Leaf Components (Weeks 1–2) Start with components that have no children. They're easiest to convert and provide quick wins.

Phase 2: Shared Services (Week 3) Convert services that leaf components depend on. Ensure providedIn: 'root' is set.

Phase 3: Feature Modules (Weeks 4–6) Convert one feature at a time. Use lazy-loaded routes to isolate changes.

Phase 4: Root Module and Bootstrap (Week 7) Finally, convert the app module and update main.ts to use bootstrapApplication.

5-Sprint Migration Plan for Enterprise Apps:

Sprint 1: Audit + Leaf Components
- Audit all modules and identify leaf components
- Migrate 30–40% of leaf components
- Update test setup for 10% of test suite

Sprint 2: More Leaf + Shared Services
- Migrate remaining leaf components
- Convert shared services to providedIn: 'root'
- Update 30% of test suite

Sprint 3: Feature Module 1
- Migrate first feature module completely
- Update all tests in that feature
- Validate lazy loading works

Sprint 4: Remaining Features
- Migrate 2–3 more feature modules
- Update 80% of test suite
- Integration testing

Sprint 5: Root App + Release
- Convert AppModule to standalone
- Update main.ts
- Full regression testing
- Deploy to production

Closing: "NgModules Are Optional, Not Gone"

Standalone components don't eliminate NgModules—they make them optional. For 2025, the Angular team's clear recommendation is: use standalone for all new code, and migrate legacy apps incrementally. The investment pays dividends in reduced boilerplate, smaller bundles, and alignment with Angular's future.

The ng generate @angular/core:standalone schematic handles most of the heavy lifting. Mixed apps (some standalone, some NgModule) are fully supported during migration, making the path risk-free. Start today, and by the end of 2025, your app will be ready for whatever the Angular team builds next.


FAQ

What are Angular standalone components?

Standalone components in Angular are components, directives, and pipes that manage their own imports directly without needing to be declared in an NgModule. They simplify the dependency graph and reduce boilerplate.

Should I migrate my Angular app to standalone components in 2025?

Yes — standalone is now the Angular default and the team recommends all new code use standalone. Migrating existing apps reduces boilerplate, improves tree-shaking, and prepares you for future Angular features built on the standalone model.

How do I migrate an Angular app to standalone components automatically?

Run ng generate @angular/core:standalone to use Angular's official schematic, which converts components, removes NgModules, and updates bootstrapApplication in three automated passes.

Can standalone and NgModule components coexist during migration?

Yes — Angular fully supports mixed apps where some components are standalone and others remain in NgModules, making incremental migration safe and production-viable.

Do standalone components affect Angular performance?

Standalone components improve tree-shaking because unused imports are not pulled in via shared NgModules, resulting in smaller bundles for most apps.