> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 16. Standalone Components Deep Dive

> Master Angular's standalone components architecture for modern, modular applications

<Frame>
  <img src="https://mintcdn.com/devweeekends/AEOaWh79Ur7CdHHv/images/courses/angular-crash-course/angular-architecture.svg?fit=max&auto=format&n=AEOaWh79Ur7CdHHv&q=85&s=d182e6a63e3b3c9b80c20d8b866c7812" alt="Standalone Components" width="1000" height="600" data-path="images/courses/angular-crash-course/angular-architecture.svg" />
</Frame>

## Standalone Components Overview

<Info>
  **Estimated Time**: 2 hours | **Difficulty**: Intermediate | **Prerequisites**: Components, Modules
</Info>

Standalone components are Angular's modern approach to building applications without NgModules. Introduced in Angular 14 and becoming the default in Angular 17+, they simplify the mental model and improve tree-shaking.

If you have ever been confused about why you had to declare a component in one module, import that module into another module, and then export the component from the first module just to use it -- standalone components are the answer. With the traditional NgModule system, the dependency graph lived in module files that were often hundreds of lines long and hard to reason about. Standalone components make each component self-describing: open the file, and the `imports` array tells you exactly what that component depends on. No more hunting through module files to figure out why something is or is not available.

```
┌─────────────────────────────────────────────────────────────────────────┐
│              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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

The `provide*()` function pattern is how Angular's own APIs are structured (`provideRouter`, `provideHttpClient`, etc.) and it is the recommended way to package your own application-wide configuration. Instead of scattering provider arrays across your codebase, you create a single function that encapsulates all the pieces a feature needs -- services, tokens, initializers -- and returns them as a unit.

```typescript theme={null}
// 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

Lazy loading was possible with NgModules, but it required wrapping components in a module just to make them lazy-loadable. With standalone components, *any* component can be lazy loaded directly via `loadComponent`. This is one of the biggest practical wins of the standalone architecture -- you no longer need to create a module for the sole purpose of enabling lazy loading.

<Note>
  **`loadComponent` vs `loadChildren`**: Use `loadComponent` when you want to lazy-load a single component (a page, a dialog). Use `loadChildren` when you want to lazy-load an entire route tree with nested child routes. Both use the same dynamic `import()` mechanism under the hood.
</Note>

### Route-Level Lazy Loading

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```
┌─────────────────────────────────────────────────────────────────────────┐
│              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                                                    │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
```

<Warning>
  **Migration pitfall**: The most common issue during NgModule-to-standalone migration is missing imports. In an NgModule world, a component might use `NgIf` without explicitly importing it because the parent module imported `CommonModule`. When you make that component standalone, you must add `CommonModule` (or the individual `NgIf` directive) to its own `imports` array. The automated migration schematic handles most cases, but always run your tests after each phase to catch what it misses.
</Warning>

### Automated Migration

```bash theme={null}
# Use Angular CLI schematic for migration -- run the three steps in order
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

```typescript theme={null}
// 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.
// This lets consuming components write `imports: [...SHARED_COMPONENTS]`
// instead of listing each one individually. Use sparingly -- it defeats
// tree-shaking if the array includes components the consumer does not use.
export const SHARED_COMPONENTS = [
  ButtonComponent,
  CardComponent
] as const;

export const SHARED_DIRECTIVES = [
  HighlightDirective
] as const;

export const SHARED_PIPES = [
  TimeAgoPipe
] as const;
```

<Note>
  **Should you use convenience arrays or individual imports?** For small teams and small shared libraries, convenience arrays are fine -- the tree-shaking impact is negligible. For published libraries or monorepos with many consumers, prefer individual imports so that unused components are eliminated from the bundle. The rule of thumb: if every consumer uses most of the shared components, arrays are convenient. If consumers cherry-pick one or two items, individual imports are better.
</Note>

***

## Best Practices

<CardGroup cols={2}>
  <Card title="Organize by Feature" icon="folder-tree">
    Group related standalone components, services, and routes by feature
  </Card>

  <Card title="Use Index Exports" icon="file-export">
    Create barrel files (index.ts) for convenient imports
  </Card>

  <Card title="Provider Functions" icon="function">
    Create reusable provider functions for complex configurations
  </Card>

  <Card title="Explicit Dependencies" icon="diagram-project">
    Import only what each component needs - improves tree-shaking
  </Card>
</CardGroup>

### Folder Structure for Standalone

```
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

<Steps>
  <Step title="Create Standalone Library">
    Build a reusable UI component library with standalone components
  </Step>

  <Step title="Migrate Existing App">
    Take a NgModule-based app and migrate it to fully standalone
  </Step>

  <Step title="Implement Lazy Loading">
    Set up advanced lazy loading patterns with preloading strategies
  </Step>
</Steps>

<Card title="Next: NgRx State Management" icon="arrow-right" href="/courses/angular-crash-course/17-ngrx">
  Master reactive state management with NgRx Store, Effects, and Selectors
</Card>
