Angular Standalone Components
Standalone components, directives, and pipes for module-free Angular development
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 linesStandalone 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 onlyRouterLinkis 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.
-
BrowserModulein Lazy-Loaded Components — importingBrowserModuleinstead ofCommonModulein a lazy-loaded component.BrowserModuleshould only appear at the application root; using it elsewhere causes duplicate platform initialization errors. -
Forgetting
standalone: truein Angular 14-16 — omitting thestandalone: trueflag 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_IMPORTSarray withoutas const, which loses type information and can silently include incorrect dependencies. Always useas constfor 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
-
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. -
Co-locate routes with features. Place
*.routes.tsfiles next to the feature components they reference. This makes lazy loading boundaries obvious. -
Use
provideXfunctions over moduleforRoot(). Angular'sprovideRouter,provideHttpClient,provideAnimations, etc., are the standalone-friendly equivalents of their module counterparts. -
Prefer
inject()over constructor injection. Theinject()function works in any injection context and reduces constructor boilerplate:private http = inject(HttpClient); -
Keep components focused. Without modules grouping related components, it is even more important to maintain a clear folder structure and naming conventions.
-
Use the Angular CLI.
ng generate componentcreates 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
BrowserModulein a lazy-loaded context. UseCommonModuleinstead.BrowserModuleshould only appear at the root level (and is automatically included when bootstrapping). -
Forgetting
standalone: truein 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
FormsModulewhen you only needNgModelpulls in more than necessary. For standalone development, prefer granular imports where the API allows it.
Install this skill directly: skilldb add angular-skills
Related Skills
Angular Testing
Testing Angular applications with Jest for unit tests and Cypress for end-to-end tests
Angular Dependency Injection
Angular dependency injection system including providers, injection tokens, and hierarchical injectors
Angular Forms
Reactive forms, form validation, dynamic forms, and typed form controls in Angular
Angular Ngrx
NgRx state management with store, effects, selectors, and the component store
Angular Routing
Angular Router configuration including lazy loading, guards, resolvers, and nested routes
Angular Rxjs Patterns
RxJS reactive patterns for data fetching, state management, and event handling in Angular