Skip to main content
Technology & EngineeringAngular264 lines

Angular Standalone Components

Standalone components, directives, and pipes for module-free Angular development

Quick Summary21 lines
You are an expert in Angular standalone components for building module-free Angular applications.

## Key Points

1. **Import only what you use.** Standalone components improve tree-shaking, but only if you import specific symbols (`RouterLink`) rather than entire modules (`RouterModule`) when possible.
2. **Co-locate routes with features.** Place `*.routes.ts` files next to the feature components they reference. This makes lazy loading boundaries obvious.
4. **Prefer `inject()` over constructor injection.** The `inject()` function works in any injection context and reduces constructor boilerplate:
5. **Keep components focused.** Without modules grouping related components, it is even more important to maintain a clear folder structure and naming conventions.
6. **Use the Angular CLI.** `ng generate component` creates standalone components by default in Angular 17+.
- **Circular imports.** Two standalone components importing each other creates a circular dependency. Extract shared logic into a third component or service.
- **Importing `BrowserModule` in a lazy-loaded context.** Use `CommonModule` instead. `BrowserModule` should only appear at the root level (and is automatically included when bootstrapping).
- **Forgetting `standalone: true` in Angular 14–16.** Components are not standalone by default until Angular 17. Omitting the flag makes them regular module-declared components.
- **Over-importing modules.** Importing `FormsModule` when you only need `NgModel` pulls in more than necessary. For standalone development, prefer granular imports where the API allows it.

## Quick Example

```typescript
private http = inject(HttpClient);
```
skilldb get angular-skills/Angular Standalone ComponentsFull skill: 264 lines
Paste into your CLAUDE.md or agent config

Standalone Components — Angular

You are an expert in Angular standalone components for building module-free Angular applications.

Core Philosophy

Standalone components are Angular's answer to the complexity that NgModules introduced. The original module system grouped components, directives, and pipes into bundles that declared, imported, and exported them — but the indirection made it hard to know which dependencies a component actually needed, and shared modules grew into grab-bags that defeated tree-shaking. Standalone components flip this by making each component explicitly declare its own imports, eliminating the module middleman.

The architectural shift is more than syntactic. Without NgModules, the application structure moves to a routes-and-providers model: routes define the loading boundaries, providers arrays on routes scope services to features, and provideX functions replace module .forRoot() patterns. This makes the dependency graph visible at the route level — you can see exactly which services a feature uses and when they are instantiated. It also makes lazy loading simpler because you no longer need to create a feature module just to get a provider scope.

Standalone components encourage thinking of each component as a self-contained unit. When a component declares imports: [RouterLink, DatePipe, SomeChildComponent], it documents its dependencies explicitly. This self-documentation makes components easier to move between projects, easier to test in isolation, and easier to understand during code review. The cost is more verbose import lists, but the benefit is clarity and tree-shakability.

Anti-Patterns

  • Importing Entire Modules When Specific Symbols Suffice — using imports: [RouterModule] when only RouterLink is needed. Importing the full module defeats tree-shaking and obscures which router features the component actually uses.

  • Circular Component Imports — two standalone components importing each other, creating a circular dependency that causes build errors. Extract shared elements into a third component or restructure the component hierarchy.

  • BrowserModule in Lazy-Loaded Components — importing BrowserModule instead of CommonModule in a lazy-loaded component. BrowserModule should only appear at the application root; using it elsewhere causes duplicate platform initialization errors.

  • Forgetting standalone: true in Angular 14-16 — omitting the standalone: true flag in component metadata, which makes the component a traditional module-declared component by default. Angular 17+ defaults to standalone, but earlier versions require the explicit flag.

  • Shared Import Arrays Without Type Safety — creating a shared COMMON_IMPORTS array without as const, which loses type information and can silently include incorrect dependencies. Always use as const for shared import arrays.

Overview

Standalone components, introduced in Angular 14 and made the default in Angular 17+, allow developers to build Angular applications without NgModule. A standalone component declares its own dependencies directly in its imports array, simplifying the mental model, improving tree-shaking, and reducing boilerplate.

Core Concepts

Declaring a Standalone Component

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-hero-list',
  standalone: true, // default in Angular 17+; explicit in 14–16
  imports: [CommonModule, RouterLink],
  template: `
    <ul>
      @for (hero of heroes; track hero.id) {
        <li><a [routerLink]="['/heroes', hero.id]">{{ hero.name }}</a></li>
      }
    </ul>
  `,
})
export class HeroListComponent {
  heroes = [
    { id: 1, name: 'Windstorm' },
    { id: 2, name: 'Bombasto' },
  ];
}

Standalone Directives and Pipes

Directives and pipes can also be standalone:

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

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective {
  private el = inject(ElementRef);

  constructor() {
    this.el.nativeElement.style.backgroundColor = 'yellow';
  }
}
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate',
  standalone: true,
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit = 50): string {
    return value.length > limit ? value.substring(0, limit) + '...' : value;
  }
}

Bootstrapping Without NgModule

Angular 17+ applications bootstrap directly with a standalone component:

// main.ts
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
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';

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

Implementation Patterns

Lazy-Loaded Standalone Routes

Standalone components enable simple lazy loading without feature modules:

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

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

Providing Services at the Route Level

Use providers in route definitions to scope services to a feature:

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    providers: [AdminService, { provide: ANALYTICS_TOKEN, useValue: 'admin' }],
    loadComponent: () =>
      import('./admin-layout.component').then(m => m.AdminLayoutComponent),
    children: [/* ... */],
  },
];

Importing Existing NgModules

Standalone components can import entire NgModules when needed:

import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';

@Component({
  standalone: true,
  imports: [MatTableModule, MatPaginatorModule],
  // ...
})
export class DataTableComponent {}

Shared Import Arrays

For commonly used imports, define a shared array:

// shared/common-imports.ts
import { CommonModule } from '@angular/common';
import { RouterLink, RouterOutlet } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';

export const COMMON_IMPORTS = [
  CommonModule,
  RouterLink,
  RouterOutlet,
  ReactiveFormsModule,
] as const;
@Component({
  standalone: true,
  imports: [...COMMON_IMPORTS, SomeOtherComponent],
  // ...
})
export class MyComponent {}

Best Practices

  1. Import only what you use. Standalone components improve tree-shaking, but only if you import specific symbols (RouterLink) rather than entire modules (RouterModule) when possible.

  2. Co-locate routes with features. Place *.routes.ts files next to the feature components they reference. This makes lazy loading boundaries obvious.

  3. Use provideX functions over module forRoot(). Angular's provideRouter, provideHttpClient, provideAnimations, etc., are the standalone-friendly equivalents of their module counterparts.

  4. Prefer inject() over constructor injection. The inject() function works in any injection context and reduces constructor boilerplate:

    private http = inject(HttpClient);
    
  5. Keep components focused. Without modules grouping related components, it is even more important to maintain a clear folder structure and naming conventions.

  6. Use the Angular CLI. ng generate component creates standalone components by default in Angular 17+.

Common Pitfalls

  • Missing imports. Unlike NgModules where declaring a component in a module makes it available to siblings, standalone components must explicitly import every dependency. Forgetting an import produces a clear compile-time error.

  • Circular imports. Two standalone components importing each other creates a circular dependency. Extract shared logic into a third component or service.

  • Importing BrowserModule in a lazy-loaded context. Use CommonModule instead. BrowserModule should only appear at the root level (and is automatically included when bootstrapping).

  • Forgetting standalone: true in Angular 14–16. Components are not standalone by default until Angular 17. Omitting the flag makes them regular module-declared components.

  • Over-importing modules. Importing FormsModule when you only need NgModel pulls in more than necessary. For standalone development, prefer granular imports where the API allows it.

Install this skill directly: skilldb add angular-skills

Get CLI access →