Standalone Components Overview
Estimated Time: 2 hours | Difficulty: Intermediate | Prerequisites: Components, Modules
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ Traditional vs Standalone Architecture │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ TRADITIONAL (NgModule-based) STANDALONE (Modern) │
│ ───────────────────────────── ───────────────────── │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ AppModule │ │ bootstrapApplication │ │
│ │ ┌─────────────────┐ │ │ │ │
│ │ │ declarations: │ │ │ ┌─────────────────┐ │ │
│ │ │ - Component1 │ │ │ │ AppComponent │ │ │
│ │ │ - Component2 │ │ │ │ standalone:true│ │ │
│ │ │ - Directive1 │ │ │ │ imports: [...] │ │ │
│ │ ├─────────────────┤ │ │ └─────────────────┘ │ │
│ │ │ imports: │ │ → │ │ │ │
│ │ │ - SharedModule │ │ │ ┌────────┴────────┐ │ │
│ │ │ - RouterModule │ │ │ ▼ ▼ │ │
│ │ └─────────────────┘ │ │ Component1 Component2│ │
│ └─────────────────────────┘ │ standalone standalone│ │
│ └─────────────────────────┘ │
│ │
│ Benefits of Standalone: │
│ • Simpler mental model - no NgModule boilerplate │
│ • Better tree-shaking - components declare their own dependencies │
│ • Easier lazy loading - any component can be lazy loaded │
│ • Improved testing - less setup required │
│ • Clear dependency graph - imports are explicit │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Creating Standalone Components
Basic Standalone Component
Copy
// user-card.component.ts
import { Component, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-user-card',
standalone: true, // ← Marks this as standalone
imports: [ // ← Declare dependencies directly
CommonModule,
RouterLink
],
template: `
<div class="user-card">
<img [src]="user().avatar" [alt]="user().name" />
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
<a [routerLink]="['/users', user().id]">View Profile</a>
<button (click)="selected.emit(user())">Select</button>
</div>
`,
styles: [`
.user-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
}
`]
})
export class UserCardComponent {
user = input.required<User>();
selected = output<User>();
}
Standalone Component with Providers
Copy
// analytics-dashboard.component.ts
@Component({
selector: 'app-analytics-dashboard',
standalone: true,
imports: [
CommonModule,
ChartComponent,
MetricCardComponent,
DateRangePickerComponent
],
providers: [
// Component-level providers
AnalyticsService,
{ provide: DATE_FORMAT, useValue: 'yyyy-MM-dd' },
{
provide: HTTP_INTERCEPTORS,
useClass: AnalyticsInterceptor,
multi: true
}
],
template: `
<div class="dashboard">
<app-date-range-picker
[range]="dateRange()"
(rangeChange)="updateRange($event)"
/>
<div class="metrics-grid">
@for (metric of metrics(); track metric.id) {
<app-metric-card [metric]="metric" />
}
</div>
<app-chart [data]="chartData()" [type]="'line'" />
</div>
`
})
export class AnalyticsDashboardComponent {
private analytics = inject(AnalyticsService);
dateRange = signal({ start: startOfMonth(new Date()), end: new Date() });
metrics = signal<Metric[]>([]);
chartData = signal<ChartData | null>(null);
constructor() {
effect(() => {
this.loadData(this.dateRange());
});
}
async loadData(range: DateRange) {
const [metrics, chart] = await Promise.all([
this.analytics.getMetrics(range),
this.analytics.getChartData(range)
]);
this.metrics.set(metrics);
this.chartData.set(chart);
}
updateRange(range: DateRange) {
this.dateRange.set(range);
}
}
Standalone Directives & Pipes
Standalone Directive
Copy
// highlight.directive.ts
import { Directive, ElementRef, input, effect, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
private el = inject(ElementRef);
color = input<string>('yellow', { alias: 'appHighlight' });
constructor() {
effect(() => {
this.el.nativeElement.style.backgroundColor = this.color();
});
}
}
// Usage - import directly where needed
@Component({
standalone: true,
imports: [HighlightDirective],
template: `
<p [appHighlight]="'lightblue'">Highlighted text</p>
`
})
export class SomeComponent {}
Standalone Pipe
Copy
// time-ago.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { formatDistanceToNow } from 'date-fns';
@Pipe({
name: 'timeAgo',
standalone: true,
pure: true
})
export class TimeAgoPipe implements PipeTransform {
transform(date: Date | string | null): string {
if (!date) return '';
const d = typeof date === 'string' ? new Date(date) : date;
return formatDistanceToNow(d, { addSuffix: true });
}
}
// relative-time.pipe.ts - Impure pipe for live updates
@Pipe({
name: 'relativeTime',
standalone: true,
pure: false // Re-evaluates on every change detection
})
export class RelativeTimePipe implements PipeTransform {
private currentTime = signal(new Date());
constructor() {
// Update every minute
setInterval(() => this.currentTime.set(new Date()), 60000);
}
transform(date: Date | string): string {
// Uses currentTime signal to trigger updates
this.currentTime(); // Subscribe to changes
return formatDistanceToNow(new Date(date), { addSuffix: true });
}
}
Bootstrapping Standalone Applications
Modern Application Bootstrap
Copy
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withViewTransitions } from '@angular/router';
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { authInterceptor } from './app/core/interceptors/auth.interceptor';
import { errorInterceptor } from './app/core/interceptors/error.interceptor';
bootstrapApplication(AppComponent, {
providers: [
// Routing
provideRouter(
routes,
withViewTransitions(),
withComponentInputBinding(),
withRouterConfig({
onSameUrlNavigation: 'reload',
paramsInheritanceStrategy: 'always'
})
),
// HTTP
provideHttpClient(
withFetch(), // Use fetch API
withInterceptors([ // Functional interceptors
authInterceptor,
errorInterceptor,
loggingInterceptor
])
),
// Animations
provideAnimationsAsync(),
// App-wide services
provideAppConfig(),
provideAuth(),
provideAnalytics()
]
}).catch(err => console.error(err));
Provider Functions Pattern
Copy
// providers/app-config.provider.ts
import { EnvironmentProviders, makeEnvironmentProviders, InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
environment: 'development' | 'staging' | 'production';
features: {
darkMode: boolean;
analytics: boolean;
newDashboard: boolean;
};
}
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
export function provideAppConfig(config?: Partial<AppConfig>): EnvironmentProviders {
const defaultConfig: AppConfig = {
apiUrl: 'https://api.example.com',
environment: 'development',
features: {
darkMode: true,
analytics: true,
newDashboard: false
}
};
return makeEnvironmentProviders([
{
provide: APP_CONFIG,
useValue: { ...defaultConfig, ...config }
},
ConfigService,
FeatureFlagService
]);
}
// providers/auth.provider.ts
export function provideAuth(options?: AuthOptions): EnvironmentProviders {
return makeEnvironmentProviders([
AuthService,
TokenStorageService,
{
provide: AUTH_CONFIG,
useValue: {
tokenKey: options?.tokenKey ?? 'auth_token',
refreshThreshold: options?.refreshThreshold ?? 300,
loginUrl: options?.loginUrl ?? '/auth/login'
}
},
{
provide: APP_INITIALIZER,
useFactory: (auth: AuthService) => () => auth.initialize(),
deps: [AuthService],
multi: true
}
]);
}
Lazy Loading Standalone Components
Route-Level Lazy Loading
Copy
// app.routes.ts
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./features/home/home.component')
.then(m => m.HomeComponent)
},
{
path: 'dashboard',
loadComponent: () => import('./features/dashboard/dashboard.component')
.then(m => m.DashboardComponent),
canActivate: [authGuard]
},
{
path: 'admin',
loadChildren: () => import('./features/admin/admin.routes')
.then(m => m.ADMIN_ROUTES),
canMatch: [adminGuard]
},
{
path: 'products',
loadChildren: () => import('./features/products/products.routes')
.then(m => m.PRODUCTS_ROUTES)
}
];
// features/admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
{
path: '',
loadComponent: () => import('./admin-layout.component')
.then(m => m.AdminLayoutComponent),
children: [
{
path: '',
loadComponent: () => import('./admin-dashboard.component')
.then(m => m.AdminDashboardComponent)
},
{
path: 'users',
loadComponent: () => import('./user-management.component')
.then(m => m.UserManagementComponent)
},
{
path: 'settings',
loadComponent: () => import('./admin-settings.component')
.then(m => m.AdminSettingsComponent)
}
]
}
];
Dynamic Component Loading
Copy
// dynamic-loader.component.ts
@Component({
selector: 'app-dynamic-loader',
standalone: true,
template: `
<ng-container #outlet />
@if (loading()) {
<div class="loading-overlay">
<app-spinner />
</div>
}
`
})
export class DynamicLoaderComponent {
componentType = input.required<string>();
componentData = input<unknown>();
@ViewChild('outlet', { read: ViewContainerRef }) outlet!: ViewContainerRef;
loading = signal(false);
private componentMap: Record<string, () => Promise<Type<any>>> = {
'chart': () => import('./components/chart.component').then(m => m.ChartComponent),
'table': () => import('./components/table.component').then(m => m.TableComponent),
'form': () => import('./components/form.component').then(m => m.FormComponent),
'widget': () => import('./components/widget.component').then(m => m.WidgetComponent)
};
constructor() {
effect(() => {
this.loadComponent(this.componentType());
});
}
private async loadComponent(type: string) {
const loader = this.componentMap[type];
if (!loader) {
console.error(`Unknown component type: ${type}`);
return;
}
this.loading.set(true);
this.outlet.clear();
try {
const component = await loader();
const componentRef = this.outlet.createComponent(component);
// Pass data to component if it has an input
if (this.componentData() && 'data' in componentRef.instance) {
componentRef.setInput('data', this.componentData());
}
} finally {
this.loading.set(false);
}
}
}
Migrating from NgModules
Migration Strategy
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ NgModule to Standalone Migration Strategy │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: Prepare │
│ ───────────────── │
│ 1. Update to Angular 17+ │
│ 2. Identify leaf components (no other components depend on them) │
│ 3. Create migration plan (bottom-up approach) │
│ │
│ Phase 2: Convert Shared Module │
│ ───────────────────────────── │
│ 1. Mark all pipes, directives, components as standalone │
│ 2. Update imports in each standalone item │
│ 3. Export individual items instead of module │
│ │
│ Phase 3: Convert Feature Modules │
│ ─────────────────────────────── │
│ 1. Convert components bottom-up │
│ 2. Replace module routing with route files │
│ 3. Update lazy loading to loadComponent/loadChildren │
│ │
│ Phase 4: Remove AppModule │
│ ────────────────────────── │
│ 1. Move providers to bootstrapApplication │
│ 2. Convert AppComponent to standalone │
│ 3. Update main.ts to use bootstrapApplication │
│ 4. Delete AppModule │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Automated Migration
Copy
# Use Angular CLI schematic for migration
ng generate @angular/core:standalone
# Options:
# --path: Specific path to migrate
# --mode: 'convert-to-standalone' | 'prune-ng-modules' | 'standalone-bootstrap'
# Step 1: Convert components to standalone
ng generate @angular/core:standalone --mode=convert-to-standalone
# Step 2: Remove unnecessary NgModules
ng generate @angular/core:standalone --mode=prune-ng-modules
# Step 3: Switch to standalone bootstrap
ng generate @angular/core:standalone --mode=standalone-bootstrap
Manual Migration Example
Copy
// BEFORE: NgModule-based
// shared.module.ts
@NgModule({
declarations: [
ButtonComponent,
CardComponent,
HighlightDirective,
TimeAgoPipe
],
imports: [CommonModule],
exports: [
ButtonComponent,
CardComponent,
HighlightDirective,
TimeAgoPipe
]
})
export class SharedModule {}
// AFTER: Standalone components
// Each component is now self-contained:
// button.component.ts
@Component({
selector: 'app-button',
standalone: true,
imports: [CommonModule],
template: `...`
})
export class ButtonComponent {}
// card.component.ts
@Component({
selector: 'app-card',
standalone: true,
imports: [CommonModule],
template: `...`
})
export class CardComponent {}
// Create an index for convenient imports
// shared/index.ts
export { ButtonComponent } from './button/button.component';
export { CardComponent } from './card/card.component';
export { HighlightDirective } from './directives/highlight.directive';
export { TimeAgoPipe } from './pipes/time-ago.pipe';
// Optional: Create a convenience array for bulk imports
export const SHARED_COMPONENTS = [
ButtonComponent,
CardComponent
] as const;
export const SHARED_DIRECTIVES = [
HighlightDirective
] as const;
export const SHARED_PIPES = [
TimeAgoPipe
] as const;
Best Practices
Organize by Feature
Group related standalone components, services, and routes by feature
Use Index Exports
Create barrel files (index.ts) for convenient imports
Provider Functions
Create reusable provider functions for complex configurations
Explicit Dependencies
Import only what each component needs - improves tree-shaking
Folder Structure for Standalone
Copy
src/app/
├── core/
│ ├── guards/
│ │ ├── auth.guard.ts
│ │ └── index.ts
│ ├── interceptors/
│ │ ├── auth.interceptor.ts
│ │ ├── error.interceptor.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── auth.service.ts
│ │ ├── api.service.ts
│ │ └── index.ts
│ └── providers/
│ ├── app-config.provider.ts
│ ├── auth.provider.ts
│ └── index.ts
├── shared/
│ ├── components/
│ │ ├── button/
│ │ │ ├── button.component.ts
│ │ │ ├── button.component.spec.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── directives/
│ ├── pipes/
│ └── index.ts
├── features/
│ ├── dashboard/
│ │ ├── components/
│ │ ├── services/
│ │ ├── dashboard.component.ts
│ │ └── dashboard.routes.ts
│ └── users/
│ ├── components/
│ ├── services/
│ ├── users.component.ts
│ └── users.routes.ts
├── app.component.ts
├── app.routes.ts
└── app.config.ts
Practice Exercise
1
Create Standalone Library
Build a reusable UI component library with standalone components
2
Migrate Existing App
Take a NgModule-based app and migrate it to fully standalone
3
Implement Lazy Loading
Set up advanced lazy loading patterns with preloading strategies
Next: NgRx State Management
Master reactive state management with NgRx Store, Effects, and Selectors