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.
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.
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.
<!-- @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.
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).
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.
<!-- 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 />
}