Back to Blog
Angular DevelopmentDecember 30, 20258 min read

Angular Standalone Components: The New Default

DR

Doug Ribeiro

8 min read

NgModule confused almost everyone who encountered Angular for the first time. I watched senior React developers spend two days understanding why a component they declared in FeatureModule was not available in another FeatureModule. The answer involved SharedModule, forFeature(), and a dependency graph that was completely invisible at the component level. Standalone components fix this by eliminating the indirection.

What Standalone Actually Means

A standalone component declares its own dependencies directly in its imports array. No NgModule wrapper. No declaration array somewhere else. If a component needs RouterLink, it imports RouterLink. If it needs CurrencyPipe, it imports CurrencyPipe. The component file is self-describing.

typescript
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { AccountCardComponent } from './account-card.component';
import { Account } from './account.model';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [
    RouterLink,
    AccountCardComponent
  ],
  template: `
    <h1>Dashboard</h1>
    <a routerLink="/accounts">All accounts</a>
    @for (account of accounts; track account.id) {
      <app-account-card [account]="account" />
    }
  `
})
export class DashboardComponent {
  accounts: Account[] = [];
}

Notice that AccountCardComponent is imported directly. It does not need to be in a shared module. The import itself tells you: this component uses AccountCardComponent. No tracing through module declarations required. Note that the imports array carries only what the template actually uses. Anything else is dead weight, and Angular's NG8113 diagnostic will tell you about it.

NgModule ApproachComponent declared in FeatureMo…FeatureModule imports SharedMod…SharedModule imports CommonModu…CommonModule provides NgIf, NgF…4 files to trace one dependencyBarrel exports required for reu…Standalone ApproachComponent imports NgIf directlyComponent imports RouterLink di…No module declaration neededDependency is visible in the co…1 file to trace one dependencyTree-shaking works at component…VS

Bootstrapping Without AppModule

New projects generated with ng new no longer create AppModule. They use bootstrapApplication with an ApplicationConfig.

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

bootstrapApplication(AppComponent, appConfig).catch(console.error);

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

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

Per-Component Lazy Loading

Before standalone, the smallest lazy-loadable unit was a module. If you wanted to lazy-load a single page component, you had to wrap it in a module first. Now you use loadComponent directly.

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

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

For feature areas with multiple routes, I still group them in a routes file and use loadChildren. That keeps the chunk boundary at the feature level. For individual pages that are genuinely standalone, loadComponent is the right call.

Scoped Providers Without Modules

NgModule providers were how you scoped services to a lazy-loaded feature. Without NgModule, you scope providers to a route via the providers array on the route definition.

typescript
{
  path: 'lending',
  loadComponent: () =>
    import('./features/lending/lending.component').then(m => m.LendingComponent),
  providers: [
    LendingService,
    { provide: LENDING_API_URL, useValue: '/api/v2/lending' }
  ]
}

In this example, the LendingService is scoped to the lending feature route. It gets created the first time the route activates, and nothing outside the lending feature can inject it. One catch: by default Angular does not destroy a route's injector when you navigate away, so the service and its state live on for the life of the app. Angular 21.1 added withExperimentalAutoCleanupInjectors, which destroys a route injector once that route leaves the active route tree, but it is experimental and opt-in. Until you turn it on, reset feature state explicitly when the route is entered instead of assuming a fresh instance.

Migrating an Existing Project

The Angular team ships schematics that automate most of the migration. Run them in sequence.

  • ng generate @angular/core:standalone --mode=convert-to-standalone: converts declarations to standalone
  • ng generate @angular/core:standalone --mode=prune-ng-modules: removes empty modules after conversion
  • ng generate @angular/core:standalone --mode=standalone-bootstrap: updates main.ts to use bootstrapApplication
  • Review the diff carefully for provider setup, especially if you use forRoot() patterns
  • Update TestBed: standalone components use imports instead of declarations in configureTestingModule

At Wonderlabs, I moved the credit card platform from Angular 13 to Angular 15 in a monorepo. Standalone APIs became stable in 15.0 and the standalone migration schematic arrived in 15.2, so that version line is where a conversion like this becomes practical. The schematic handles most of the work. What remains is provider configuration in AppModule that needs manual translation to provideX() functions. Budget half a day per project.

The Practical Impact

After migrating, the most immediate change is onboarding speed. A new developer can understand what a component depends on by reading the component file. They no longer need to know which module it lives in, which SharedModule exports CommonModule, and why RouterModule needs to be imported in three places. That cognitive overhead was real, and it is gone.

Build size also improves in projects where module boundaries were coarse. Tree-shaking is more precise when the import graph lives at the component level. In a feature area that leans on an overly broad SharedModule, the initial bundle can get noticeably smaller after migration just from eliminating the imports that module was pulling in.

Standalone components did not add new capabilities. They removed indirection. That is a harder engineering problem to solve, and it matters more.

References

AngularTypeScriptFrontendBest Practices