Angular has evolved significantly with versions 14 through 17. The introduction of Standalone Components and Signals has drastically simplified the framework, making NgModules optional and revolutionizing change detection. This guide covers the modern, enterprise-ready Angular patterns.


Step 1 — Standalone Components

Standalone components are the new default in Angular. They remove the need for NgModules by declaring their own dependencies directly in the @Component decorator.

user-card.component.tstypescript
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { UserAvatarComponent } from './user-avatar.component';

@Component({
  selector: 'app-user-card',
  standalone: true,
  // Import dependencies directly into the component
  imports: [CommonModule, RouterModule, UserAvatarComponent],
  template: `
    <div class="card">
      <!-- *ngIf is imported via CommonModule -->
      <app-user-avatar *ngIf="user" [src]="user.avatar"></app-user-avatar>
      <h3>{{ user?.name }}</h3>
      <a [routerLink]="['/users', user?.id]">View Profile</a>
    </div>
  `,
  styles: [`.card { padding: 1rem; border: 1px solid #ccc; }`]
})
export class UserCardComponent {
  @Input() user: any;
}

Step 2 — Signals (Modern Reactivity)

Signals (introduced in Angular 16+) provide fine-grained reactivity without relying on Zone.js. A signal is a wrapper around a value that notifies interested consumers when that value changes.

counter.component.tstypescript
import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <!-- Read signal value with parentheses () -->
    <h2>Count: {{ count() }}</h2>
    <h3>Double: {{ doubleCount() }}</h3>
    <button (click)="increment()">Increment</button>
    <button (click)="reset()">Reset</button>
  `
})
export class CounterComponent {
  // 1. Writable Signal
  count = signal(0);

  // 2. Computed Signal (read-only, memoized)
  doubleCount = computed(() => this.count() * 2);

  constructor() {
    // 3. Effect: runs whenever a read signal changes
    effect(() => {
      console.log(`The count is now: ${this.count()}`);
    });
  }

  increment() {
    // Update signal value
    this.count.update(c => c + 1);
  }

  reset() {
    // Set signal directly
    this.count.set(0);
  }
}

Step 3 — Control Flow Syntax (Angular 17+)

Angular 17 replaced *ngIf and *ngFor with built-in control flow syntax. It is much faster and doesn't require importing CommonModule.

template.htmlhtml
<!-- @if / @else -->
@if (isLoggedIn()) {
  <user-profile [user]="user()" />
} @else {
  <login-form />
}

<!-- @for with mandatory tracking -->
<ul>
  @for (item of items(); track item.id) {
    <li>{{ item.name }}</li>
  } @empty {
    <li>No items found.</li>
  }
</ul>

<!-- @switch -->
@switch (status()) {
  @case ('loading') {
    <spinner />
  }
  @case ('success') {
    <success-message />
  }
  @default {
    <error-message />
  }
}

Step 4 — Dependency Injection & inject()

The new inject() function is the modern way to consume services, replacing constructor injection. It works seamlessly with class inheritance and regular functions.

data.service.tstypescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root' // Singleton across the app
})
export class DataService {
  // Modern DI using inject()
  private http = inject(HttpClient);

  getUsers() {
    return this.http.get<User[]>('/api/users');
  }
}

// Usage in component
export class UserList {
  private dataService = inject(DataService);
  users$ = this.dataService.getUsers();
}

Step 5 — RxJS Fundamentals in Angular

Angular heavily utilizes RxJS for handling asynchronous data streams (HttpClient, Router events, Reactive Forms).

search.component.tstypescript
import { Component, inject } from '@angular/core';
import { FormControl } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap, catchError, of } from 'rxjs';

export class SearchComponent {
  searchControl = new FormControl('');
  private http = inject(HttpClient);

  // RxJS pipeline
  results$ = this.searchControl.valueChanges.pipe(
    debounceTime(300),          // wait 300ms after last keystroke
    distinctUntilChanged(),     // only emit if value is different
    switchMap(term =>           // cancel previous pending request if new one starts
      this.http.get(`/api/search?q=${term}`).pipe(
        catchError(() => of([])) // handle errors gracefully
      )
    )
  );
}

Step 6 — Performance: Deferable Views

Angular 17 introduced @defer, enabling declarative lazy loading of component dependencies directly in the template.

defer.htmlhtml
<!-- The chart component will only be loaded when the user scrolls it into view -->
@defer (on viewport) {
  <heavy-chart-component [data]="chartData" />
} @placeholder {
  <div>Chart will load when you scroll here...</div>
} @loading (minimum 1s) {
  <spinner />
} @error {
  <p>Failed to load chart.</p>
}

<!-- Load immediately when a signal condition is met -->
@defer (when isReady()) {
  <dashboard />
}