Skip to main content

4 - Standalone Components: Angular Without NgModules

Learn how to develop in Angular without NgModules: standalone components, routing with loadComponent, bootstrapApplication(), and automatic migration.

What do you think about this article?

At a Glance

Learn how to develop in Angular without NgModules: standalone components, routing with loadComponent, bootstrapApplication(), and automatic migration.

Standalone Components: Angular Without NgModules

With the introduction of Standalone Components in Angular 14 and their progressive adoption as the standard starting from Angular 15, the framework underwent a fundamental architectural shift: removing the requirement to use NgModules for code organization. This evolution drastically simplified development, reduced boilerplate, and made Angular far more accessible to newcomers.

In this fourth article of the Modern Angular series, we will explore Standalone Components in depth: from the problem they solve to complete migration, covering routing, bootstrapping, dependency injection, and advanced patterns. If you have ever struggled with declarations, imports, and exports in Angular modules, this article will show you how to leave them behind for good.

What You Will Learn

  • Why NgModules were a problem and how Standalone Components solve it
  • How to create standalone components, directives, and pipes
  • Managing dependencies with imports directly in the component
  • Configuring routing and lazy loading without modules
  • Bootstrapping the application with bootstrapApplication()
  • Dependency Injection in the standalone world
  • Migrating an existing project with the official schematic
  • Advanced patterns and best practices for enterprise applications

Modern Angular Series Overview

# Article Topic
1Angular SignalsFine-grained reactivity
2Zoneless Change DetectionRemoving Zone.js
3New Template Syntax: @if, @for, @deferControl flow blocks
4You are here - Standalone ComponentsModule-free architecture
5Signal FormsReactive forms with Signals
6Angular SSR and Incremental HydrationServer-side rendering
7Core Web Vitals in AngularPerformance optimization
8Angular PWAProgressive Web Apps
9Advanced Dependency InjectionDI patterns and tokens
10Migrating from Angular 17 to 21Step-by-step migration

1. The NgModule Problem

Since version 2, Angular has used NgModules as the fundamental unit for code organization. Every component, directive, or pipe had to be declared in exactly one module via the declarations array, and inter-module dependencies were managed through imports and exports. While this system provided clear structure, it generated increasingly severe problems as applications grew in complexity.

Typical NgModule with extensive boilerplate

// app.module.ts - The traditional approach
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { FooterComponent } from './footer/footer.component';
import { HomeComponent } from './home/home.component';
import { ProductListComponent } from './products/product-list.component';
import { ProductDetailComponent } from './products/product-detail.component';
import { CartComponent } from './cart/cart.component';
import { CheckoutComponent } from './checkout/checkout.component';
import { HighlightDirective } from './directives/highlight.directive';
import { CurrencyPipe } from './pipes/currency.pipe';

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    FooterComponent,
    HomeComponent,
    ProductListComponent,
    ProductDetailComponent,
    CartComponent,
    CheckoutComponent,
    HighlightDirective,
    CurrencyPipe
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    ReactiveFormsModule,
    RouterModule.forRoot(routes)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
    

Common Problems with NgModules

  • Excessive boilerplate: Every component requires import and declaration in a module. Large projects can have hundreds of lines just for declarations
  • Circular dependencies: When two modules import each other, hard-to-diagnose errors arise
  • Declarations vs imports confusion: New developers constantly confuse where to put components (declarations) and where to put modules (imports)
  • "Component is not part of any NgModule" error: One of the most frustrating errors for beginners, caused by forgetting to declare a component
  • Tight coupling: Components are coupled to the module that declares them, making reuse and unit testing more difficult
  • Complex lazy loading: To lazy load a feature, you needed to create a dedicated module with its own routing configuration

Consider a typical circular dependency example that plagued many Angular projects:

Circular dependency between modules

// shared.module.ts
@NgModule({
  declarations: [UserAvatarComponent],
  imports: [ProductModule],  // Imports ProductModule
  exports: [UserAvatarComponent]
})
export class SharedModule { }

// product.module.ts
@NgModule({
  declarations: [ProductCardComponent],
  imports: [SharedModule],   // Imports SharedModule → CIRCULAR!
  exports: [ProductCardComponent]
})
export class ProductModule { }
    

NgModules vs Standalone: Quick Comparison

Aspect NgModules Standalone
Component declaration In module's declarations Self-contained with standalone: true
Dependency management Through intermediate modules Directly in the component
Lazy loading Requires dedicated NgModule Direct loadComponent()
Boilerplate High (module + declarations) Minimal
Unit testing TestBed with test module setup Minimal configuration
Circular dependencies Frequent between modules Nearly impossible
Learning curve Steep (declarations, imports, exports, providers, bootstrap) Gentle (standalone: true, imports)

2. What Are Standalone Components

A Standalone Component is an Angular component that declares itself as self-sufficient through the standalone: true flag in the @Component decorator. This means the component does not need to be declared in any NgModule: it manages its own dependencies directly through its imports array.

Starting with Angular 19, all components, directives, and pipes are standalone by default. You do not even need to specify standalone: true because it is the default behavior. This is the definitive signal that the Angular team considers Standalone Components the future of the framework.

Standalone Component: before and after Angular 19

// Angular 14-18: explicit standalone flag
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `<div class="user-card">
    <h3>{{ user().name }}</h3>
    <p>{{ user().email }}</p>
  </div>`,
  styles: [`
    .user-card {
      padding: 1rem;
      border: 1px solid #ccc;
      border-radius: 8px;
    }
  `]
})
export class UserCardComponent {
  user = input.required<User>();
}

// Angular 19+: standalone is the default, no need to specify it
@Component({
  selector: 'app-user-card',
  template: `<div class="user-card">
    <h3>{{ user().name }}</h3>
    <p>{{ user().email }}</p>
  </div>`,
  styles: [`
    .user-card {
      padding: 1rem;
      border: 1px solid #ccc;
      border-radius: 8px;
    }
  `]
})
export class UserCardComponent {
  user = input.required<User>();
}
    

Note on standalone: true in Angular 19+

From Angular 19 onward, if you omit the standalone property, the component is standalone by default. If you explicitly want to create a non-standalone component (for compatibility with legacy code), you must specify standalone: false. Throughout this article we will use standalone: true for clarity, but remember that in Angular 21 it is optional.

3. Creating Standalone Components, Directives, and Pipes

The "standalone" concept does not apply only to components. Directives and pipes can also be standalone, following the same principle of self-sufficiency.

Standalone Component

Complete standalone component

import { Component, input, output, signal } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-todo-item',
  standalone: true,
  imports: [CommonModule],  // Dependencies managed here
  template: `
    <div class="todo-item" [class.completed]="todo().completed">
      <input type="checkbox"
             [checked]="todo().completed"
             (change)="onToggle()" />
      <span>{{ todo().title }}</span>
      <button (click)="onDelete()">Delete</button>
    </div>
  `
})
export class TodoItemComponent {
  todo = input.required<Todo>();
  toggled = output<number>();
  deleted = output<number>();

  onToggle() {
    this.toggled.emit(this.todo().id);
  }

  onDelete() {
    this.deleted.emit(this.todo().id);
  }
}
    

Standalone Directive

Standalone directive for element highlighting

import { Directive, ElementRef, input, effect } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  appHighlight = input<string>('yellow');

  constructor(private el: ElementRef) {
    effect(() => {
      this.el.nativeElement.style.backgroundColor = this.appHighlight();
    });
  }
}
    

Standalone Pipe

Standalone pipe for relative date formatting

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

@Pipe({
  name: 'timeAgo',
  standalone: true
})
export class TimeAgoPipe implements PipeTransform {
  transform(value: Date): string {
    const seconds = Math.floor((new Date().getTime() - value.getTime()) / 1000);

    if (seconds < 60) return 'just now';
    if (seconds < 3600) return Math.floor(seconds / 60) + ' minutes ago';
    if (seconds < 86400) return Math.floor(seconds / 3600) + ' hours ago';
    return Math.floor(seconds / 86400) + ' days ago';
  }
}
    

4. Managing Dependencies with imports

The key to Standalone Components is the imports array in the @Component decorator. Here you declare all template dependencies: other components, directives, pipes, and even entire NgModules (for compatibility with legacy libraries). Each component knows exactly what it needs, with no intermediaries.

Dependency management in the component

import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import { AsyncPipe, DatePipe, CurrencyPipe } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';

// Import standalone components directly
import { UserCardComponent } from './user-card.component';
import { LoadingSpinnerComponent } from '../shared/loading-spinner.component';
import { HighlightDirective } from '../directives/highlight.directive';
import { TimeAgoPipe } from '../pipes/time-ago.pipe';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [
    // Angular modules (for compatibility)
    ReactiveFormsModule,

    // Standalone pipes
    AsyncPipe,
    DatePipe,
    CurrencyPipe,
    TimeAgoPipe,

    // Standalone components
    UserCardComponent,
    LoadingSpinnerComponent,

    // Standalone directives
    HighlightDirective,

    // Router
    RouterLink,
    RouterOutlet
  ],
  templateUrl: './dashboard.component.html'
})
export class DashboardComponent {
  // The component knows all its dependencies
}
    

Comparison: Dependencies with NgModules vs Standalone

With NgModules With Standalone
Component declared in a module Component is self-sufficient
Dependencies imported in the module Dependencies imported in the component
All module declarations see the imports Only the component sees its imports
Risk of unused imports in the module Every import is explicit and necessary

Warning: Do Not Import More Than Needed

With Standalone Components you might be tempted to import the entire CommonModule for convenience. However, it is better to import only the individual pipes and directives you need (e.g., NgIf, NgFor, AsyncPipe). With the new @if and @for syntax from Angular 17+, many of these imports are no longer necessary because control flow is built into the template natively.

5. Routing Without NgModules

One of the biggest advantages of Standalone Components is the simplification of routing. With modules, lazy loading a feature required creating a dedicated module with RouterModule.forChild(). With Standalone Components, you can lazy load a single component directly using loadComponent.

loadComponent: Lazy Loading a Single Component

Routing with standalone lazy loading

// app.routes.ts - Complete routing configuration without modules
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./home/home.component').then(m => m.HomeComponent)
  },
  {
    path: 'products',
    loadComponent: () =>
      import('./products/product-list.component').then(m => m.ProductListComponent)
  },
  {
    path: 'products/:id',
    loadComponent: () =>
      import('./products/product-detail.component').then(m => m.ProductDetailComponent)
  },
  {
    path: 'cart',
    loadComponent: () =>
      import('./cart/cart.component').then(m => m.CartComponent)
  },
  {
    path: '**',
    loadComponent: () =>
      import('./not-found/not-found.component').then(m => m.NotFoundComponent)
  }
];
    

loadChildren: Lazy Loading a Group of Routes

For complex features with sub-routes, you can use loadChildren pointing to a file that exports a Routes array, with no need for a module wrapper.

Lazy loading standalone sub-routes

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

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

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./admin-layout.component').then(m => m.AdminLayoutComponent),
    children: [
      {
        path: 'dashboard',
        loadComponent: () =>
          import('./dashboard/dashboard.component').then(m => m.DashboardComponent)
      },
      {
        path: 'users',
        loadComponent: () =>
          import('./users/users.component').then(m => m.UsersComponent)
      },
      {
        path: 'settings',
        loadComponent: () =>
          import('./settings/settings.component').then(m => m.SettingsComponent)
      }
    ]
  }
];
    

Lazy Loading: NgModules vs Standalone

Approach NgModules Standalone
Lazy load single component Impossible without module wrapper loadComponent: () => import(...)
Lazy load feature loadChildren with NgModule loadChildren with Routes array
Files required feature.module.ts + feature-routing.module.ts feature.routes.ts (only)
Granularity Module level Individual component level

6. Bootstrapping with bootstrapApplication()

With Standalone Components, the way you bootstrap the application also changes radically. You no longer need an AppModule with the platformBrowserDynamic().bootstrapModule() method. Instead, you use bootstrapApplication(), which accepts the root component and a provider configuration.

The Old Approach with NgModule

Traditional main.ts with NgModule

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

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

The New Standalone Approach

main.ts with bootstrapApplication()

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

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));
    
app.config.ts - Centralized provider configuration

// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withViewTransitions } from '@angular/router';
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    // Change detection (or provideExperimentalZonelessChangeDetection())
    provideZoneChangeDetection({ eventCoalescing: true }),

    // Router with view transitions
    provideRouter(routes, withViewTransitions()),

    // HTTP client with functional interceptors
    provideHttpClient(
      withFetch(),
      withInterceptors([authInterceptor, errorInterceptor])
    ),

    // Async animations
    provideAnimationsAsync(),

    // Hydration for SSR
    provideClientHydration(withEventReplay())
  ]
};
    

The provide* Functions

With the standalone approach, Angular introduces provide* functions that replace the old configuration modules. Here are the main ones:

provide* Function Replaces
provideRouter() RouterModule.forRoot()
provideHttpClient() HttpClientModule
provideAnimationsAsync() BrowserAnimationsModule
provideClientHydration() SSR configuration in module
provideZoneChangeDetection() Implicit Zone.js configuration

7. Dependency Injection in the Standalone World

Angular's Dependency Injection (DI) works perfectly with Standalone Components, but offers new patterns for configuring providers at different levels of the application.

providedIn: 'root' - The Recommended Pattern

The simplest and recommended way to provide a service is to use providedIn: 'root' in the @Injectable decorator. This registers the service as a singleton in the root injector, available everywhere in the application. The advantage is that the service is tree-shakable: if no component injects it, it gets removed from the production bundle.

Service with providedIn: 'root'

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

@Injectable({
  providedIn: 'root'  // Tree-shakable singleton
})
export class CartService {
  private _items = signal<CartItem[]>([]);
  readonly items = this._items.asReadonly();

  readonly totalItems = computed(() => this._items().length);
  readonly totalPrice = computed(() =>
    this._items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );

  addItem(product: Product) {
    this._items.update(items => [...items, {
      ...product,
      quantity: 1
    }]);
  }

  removeItem(id: number) {
    this._items.update(items => items.filter(item => item.id !== id));
  }
}
    

Route-Level Providers

For services that should only be shared within a feature, you can define providers directly in the route configuration. This creates a dedicated environment injector for that section of the application.

Route-level providers

// admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    providers: [
      // This service is available ONLY in admin components
      AdminAuthService,
      // Configurable providers also work
      {
        provide: API_BASE_URL,
        useValue: '/api/admin'
      }
    ],
    children: [
      {
        path: 'dashboard',
        loadComponent: () =>
          import('./dashboard.component').then(m => m.DashboardComponent)
      },
      {
        path: 'users',
        loadComponent: () =>
          import('./users.component').then(m => m.UsersComponent)
      }
    ]
  }
];
    

Component-Level Providers

Providers in the standalone component

@Component({
  selector: 'app-editor',
  standalone: true,
  providers: [
    // New instance for every editor component
    EditorStateService,
    {
      provide: UNDO_STACK_SIZE,
      useValue: 50
    }
  ],
  template: `...`
})
export class EditorComponent {
  private editorState = inject(EditorStateService);
}
    

Provider Hierarchy in the Standalone World

Level Configuration Scope Use Case
Root providedIn: 'root' Entire app (singleton) AuthService, HttpService, StateStore
App Config appConfig.providers Entire app Router, HttpClient, Animations
Route route.providers Feature/section AdminService, FeatureConfig
Component component.providers Single component FormState, EditorState

8. Migrating from NgModules to Standalone

Angular provides an official migration schematic that automates most of the conversion process from NgModules to Standalone Components. The migration occurs in three progressive phases, allowing you to proceed safely and incrementally.

Phase 1: Convert Components to Standalone

The first step converts all components, directives, and pipes to standalone by adding standalone: true and the necessary imports.

Running the automated migration - Phase 1

# Phase 1: Convert components, directives, and pipes to standalone
ng generate @angular/core:standalone-migration --mode=convert-to-standalone

# What it does:
# - Adds standalone: true to all components/directives/pipes
# - Adds the necessary imports in each component's decorator
# - Keeps existing NgModules for compatibility
    

Phase 2: Remove Unnecessary NgModules

Running the automated migration - Phase 2

# Phase 2: Remove NgModules that are no longer needed
ng generate @angular/core:standalone-migration --mode=prune-ng-modules

# What it does:
# - Removes NgModules that contain only standalone declarations
# - Updates imports in files that referenced them
# - Keeps modules that define providers or other configurations
    

Phase 3: Switch to bootstrapApplication()

Running the automated migration - Phase 3

# Phase 3: Convert the application bootstrap
ng generate @angular/core:standalone-migration --mode=standalone-bootstrap

# What it does:
# - Converts main.ts to use bootstrapApplication()
# - Creates app.config.ts with providers
# - Removes AppModule if no longer necessary
# - Migrates providers from the module to the configuration
    

Tips for a Safe Migration

  • Commit before each phase: This way you can easily roll back if something goes wrong
  • Run tests after each phase: Make sure the application works correctly before proceeding
  • Check for circular dependencies: The migration may reveal hidden circular dependencies in modules
  • Review providers: Some modules define providers with forRoot() and forChild() that require manual attention
  • Third-party libraries: Verify that the libraries you use support standalone. Most do by exporting standalone components or providing provide*() functions

Manual Step-by-Step Migration

If you prefer to migrate manually, here is the typical process for converting a component:

Before: Component declared in a module

// BEFORE: product-card.component.ts (in an NgModule)
@Component({
  selector: 'app-product-card',
  templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
  @Input() product!: Product;
  @Output() addToCart = new EventEmitter<Product>();
}

// product.module.ts
@NgModule({
  declarations: [ProductCardComponent, ProductListComponent],
  imports: [CommonModule, RouterModule],
  exports: [ProductCardComponent]
})
export class ProductModule { }
    
After: Modern standalone component

// AFTER: product-card.component.ts (standalone)
import { Component, input, output } from '@angular/core';
import { RouterLink } from '@angular/router';
import { CurrencyPipe } from '@angular/common';

@Component({
  selector: 'app-product-card',
  standalone: true,
  imports: [RouterLink, CurrencyPipe],
  templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
  product = input.required<Product>();
  addToCart = output<Product>();
}

// product.module.ts → DELETED!
// Routes point directly to the components
    

9. Advanced Patterns

Barrel Exports for Shared Components

When you have many shared standalone components, it is useful to create a barrel file (index.ts) that re-exports them all. This simplifies imports in the components that use them.

Barrel export for shared components

// shared/ui/index.ts - Barrel export
export { ButtonComponent } from './button/button.component';
export { CardComponent } from './card/card.component';
export { ModalComponent } from './modal/modal.component';
export { TabsComponent } from './tabs/tabs.component';
export { TooltipDirective } from './tooltip/tooltip.directive';
export { TimeAgoPipe } from './pipes/time-ago.pipe';
export { TruncatePipe } from './pipes/truncate.pipe';

// Usage in a component:
import {
  ButtonComponent,
  CardComponent,
  TooltipDirective,
  TruncatePipe
} from '../shared/ui';

@Component({
  standalone: true,
  imports: [ButtonComponent, CardComponent, TooltipDirective, TruncatePipe],
  // ...
})
export class MyFeatureComponent { }
    

Composing Common Import Arrays

If multiple components share the same dependencies, you can create a common imports array to reuse. This is a lightweight pattern that replaces SharedModules.

Shared import arrays

// shared/common-imports.ts
import { RouterLink } from '@angular/router';
import { DatePipe, CurrencyPipe, AsyncPipe } from '@angular/common';
import { ButtonComponent } from './ui/button/button.component';
import { CardComponent } from './ui/card/card.component';
import { LoadingSpinnerComponent } from './ui/loading-spinner/loading-spinner.component';

export const COMMON_IMPORTS = [
  RouterLink,
  DatePipe,
  CurrencyPipe,
  AsyncPipe,
  ButtonComponent,
  CardComponent,
  LoadingSpinnerComponent
] as const;

// Usage:
@Component({
  standalone: true,
  imports: [...COMMON_IMPORTS, ReactiveFormsModule],
  // ...
})
export class CheckoutComponent { }
    

Granular Lazy Loading with @defer

By combining Standalone Components with the @defer block introduced in Angular 17, you can achieve even more granular lazy loading at the template level:

Template-level lazy loading with @defer

<!-- The HeavyChart component loads only when it enters the viewport -->
@defer (on viewport) {
  <app-heavy-chart [data]="chartData()" />
} @placeholder {
  <div class="chart-placeholder">Loading chart...</div>
} @loading (minimum 500ms) {
  <app-loading-spinner />
}

<!-- The Comments component loads only on interaction -->
@defer (on interaction) {
  <app-comments [postId]="postId()" />
} @placeholder {
  <button>Show comments</button>
}
    

Benefits of Granular Lazy Loading

With NgModules, the minimum granularity for lazy loading was an entire module with all its components. With Standalone Components, every single component can be loaded independently, either through the router (loadComponent) or through the template (@defer). This significantly reduces the initial bundle and improves loading times.

10. Best Practices

After exploring all the features of Standalone Components, here are the best practices to follow for organizing code in modern Angular applications.

The 10 Golden Rules of Standalone Components

  1. Standalone by default: Always create standalone components. Use NgModules only for compatibility with legacy libraries
  2. Import only what you need: Avoid importing entire modules like CommonModule. Import individual pipes and directives
  3. Use the new template syntax: With @if, @for, and @switch you no longer need to import NgIf, NgFor, NgSwitch
  4. One component, one file: Each standalone component should have its own file. Avoid declaring multiple components in the same file
  5. Barrel exports for features: Use index.ts files to re-export shared components for a feature
  6. Providers in routes, not components: For feature services, prefer route.providers over component.providers
  7. providedIn: 'root' for global services: Keep providedIn: 'root' for singleton, tree-shakable services
  8. Aggressive lazy loading: Use loadComponent in the router and @defer in templates to load components on demand
  9. Migrate gradually: You do not have to migrate everything at once. Standalone and NgModules can coexist indefinitely
  10. Test every component in isolation: A major advantage of standalone is ease of testing. Make the most of it!

Recommended Project Structure

Structure of a standalone Angular project

src/app/
  app.component.ts          # Root component (standalone)
  app.config.ts             # ApplicationConfig with providers
  app.routes.ts             # Main routes

  core/                     # Singleton services and guards
    services/
      auth.service.ts       # providedIn: 'root'
      api.service.ts        # providedIn: 'root'
    interceptors/
      auth.interceptor.ts
    guards/
      auth.guard.ts

  shared/                   # Reusable components, directives, pipes
    ui/
      button/
        button.component.ts     # standalone
      card/
        card.component.ts       # standalone
      modal/
        modal.component.ts      # standalone
      index.ts                  # barrel export
    directives/
      tooltip.directive.ts      # standalone
    pipes/
      time-ago.pipe.ts          # standalone

  features/                 # Application features
    products/
      product-list.component.ts   # standalone
      product-detail.component.ts # standalone
      product-card.component.ts   # standalone (local)
      products.routes.ts          # sub-routes
    admin/
      admin-layout.component.ts
      dashboard.component.ts
      admin.routes.ts
    

Testing Standalone Components

One of the most significant advantages is simplified testing. You no longer need to configure a complex test module with TestBed: the component carries all its dependencies with it.

Simplified testing with standalone

// product-card.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductCardComponent } from './product-card.component';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      // With standalone, just import the component!
      imports: [ProductCardComponent]
      // No need for: declarations, module imports, etc.
    }).compileComponents();

    fixture = TestBed.createComponent(ProductCardComponent);
  });

  it('should display the product name', () => {
    // Use ComponentRef to set inputs
    fixture.componentRef.setInput('product', {
      id: 1,
      name: 'Angular T-Shirt',
      price: 29.99
    });
    fixture.detectChanges();

    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.textContent).toContain('Angular T-Shirt');
  });

  it('should emit addToCart when the button is clicked', () => {
    fixture.componentRef.setInput('product', {
      id: 1,
      name: 'Angular T-Shirt',
      price: 29.99
    });
    fixture.detectChanges();

    const spy = jest.spyOn(fixture.componentInstance.addToCart, 'emit');
    const button = fixture.nativeElement.querySelector('button');
    button.click();

    expect(spy).toHaveBeenCalledWith(expect.objectContaining({
      id: 1
    }));
  });
});
    

When to Keep NgModules

Even though Standalone Components are the future, there are cases where NgModules are still useful or necessary:

  • Third-party libraries: Some libraries still export NgModules. You can import them directly in the standalone component's imports array
  • Large legacy projects: If you have a project with hundreds of components, migration can be gradual. You do not need to do everything at once
  • SharedModule with many dependencies: If you have a SharedModule that imports dozens of components, it may be more practical to keep it temporarily and replace it with a barrel export when you are ready

Summary and Next Steps

In this article we explored Angular Standalone Components in depth: from the motivation to migration, from routing to dependency injection. Here are the key takeaways:

Key Concepts

  • Standalone Components eliminate the need for NgModules, making each component self-sufficient
  • From Angular 19, standalone is the default behavior for components, directives, and pipes
  • The imports array in the decorator allows declaring dependencies directly in the component
  • loadComponent and loadChildren simplify lazy loading without module wrappers
  • bootstrapApplication() and ApplicationConfig replace module-based bootstrapping
  • The provide*() functions replace configuration modules (RouterModule.forRoot(), etc.)
  • DI works at four levels: root, app config, route, and component
  • The migration schematic automates the conversion in three progressive phases
  • Barrel exports and common import arrays replace SharedModules

In the next article in the series, Signal Forms, we will see how the Signals revolution extends to form management, with a modern alternative to Reactive Forms that leverages fine-grained reactivity to create performant and declarative forms.

Frequently asked questions

What is the main difference between Standalone Components and NgModules in Angular?

The main difference is that Standalone Components do not require declaration in an NgModule, whereas NgModules were previously used as the fundamental unit for code organization.

From which version of Angular are all components, directives, and pipes standalone by default?

Starting with Angular 19, all components, directives, and pipes are standalone by default.

How do you declare dependencies in a Standalone Component using the imports array in the decorator?

You declare template dependencies, such as other components, directives, pipes, and even entire NgModules (for compatibility with legacy libraries), directly in the imports array of the component's decorator.

What is the recommended way to provide a service in a Standalone Component using Dependency Injection?

The simplest and recommended way is to use the providedIn: 'root' property in the decorator, which registers the service as a singleton in the root injector.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects