DevToolBoxGRATUIT
Blog

Guide Angular: Composants, Services, RxJS, NgRx et Signaux Angular 17+

14 min de lecturepar DevToolBox
TL;DR

Angular is a complete, opinionated TypeScript-first frontend framework maintained by Google. Unlike React (a library) or Vue (a progressive framework), Angular gives you everything out of the box: component system, two-way data binding, dependency injection, HTTP client, router, reactive forms, and a powerful CLI. It has a steeper learning curve but excels in large enterprise applications where structure and consistency matter. Angular 17+ introduced standalone components, signals, and a new control flow syntax that dramatically simplify development.

Angular is a battle-tested, TypeScript-first web application framework built and maintained by Google. First released as AngularJS in 2010 and completely rewritten as Angular 2 in 2016, it is now on version 17+ with a rapid release cadence of two major versions per year. Angular is the framework of choice for large enterprise applications at companies like Google, Microsoft, IBM, and many Fortune 500 firms. Its opinionated nature — enforcing TypeScript, providing a complete toolchain, and mandating architectural patterns — makes it ideal for large teams where consistency and maintainability are critical. This guide covers everything you need to know, from components and directives to NgRx state management and the latest Angular 17+ features.

Key Takeaways
  • Angular is a full framework, not a library — it provides opinionated solutions for routing, forms, HTTP, testing, and state management without third-party decisions.
  • Every Angular application is built from components — a component is a TypeScript class with a @Component decorator, a template, and optional styles.
  • Dependency injection is central to Angular — services are singleton classes provided at module or root level, injected via constructor parameters.
  • Use reactive forms (FormGroup/FormControl) over template-driven forms for complex scenarios — they are easier to test and provide synchronous access to form values.
  • RxJS Observables power Angular — the async pipe handles subscriptions automatically, preventing memory leaks in templates.
  • Standalone components (Angular 14+) eliminate the need for NgModule for most use cases, making Angular apps smaller and simpler to reason about.

What is Angular? The Full-Framework Philosophy

Angular differs fundamentally from React and Vue in its design philosophy. React is a UI rendering library — you compose it with third-party tools for routing (React Router), state (Redux/Zustand), and HTTP (Axios/fetch). Vue is a progressive framework that starts simple and scales up. Angular is a complete, batteries-included framework: it ships with a router, HTTP client, reactive forms, animation system, internationalization, testing utilities, and a CLI. This means more initial complexity but less decision fatigue and more consistency across large codebases.

Angular CLI: The Essential Toolchain

The Angular CLI (@angular/cli) is the primary way to create, build, test, and deploy Angular applications. It provides code generation (ng generate), a development server with hot module replacement, optimized production builds with tree shaking and differential loading, unit test runner (Karma/Jest), and end-to-end test runner (Cypress/Playwright).

# Install Angular CLI globally
npm install -g @angular/cli

# Create a new Angular application
ng new my-app --routing --style=scss

# Start development server
ng serve --open

# Generate a component
ng generate component features/user-profile
ng g c features/user-profile  # shorthand

# Generate a service
ng generate service services/auth

# Build for production
ng build --configuration production

# Run unit tests
ng test

# Run end-to-end tests
ng e2e

TypeScript-First Architecture

Angular was designed from scratch with TypeScript in mind. Type safety is not optional — Angular relies on TypeScript decorators (@Component, @Injectable, @Input, @Output) for its metadata system. The Angular compiler (Ivy) performs ahead-of-time (AOT) compilation, converting TypeScript and templates into optimized JavaScript before the browser runs them. This enables better error detection at build time, smaller bundle sizes, and faster runtime performance.

Angular Components: The Building Blocks

A component is the fundamental building block of an Angular application. Every visible element in Angular is a component. A component consists of three parts: a TypeScript class that defines logic and data, an HTML template that defines the view, and CSS/SCSS styles that define the appearance. Components are decorated with @Component, which provides Angular with the metadata it needs to create and render the component.

// user-card.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
      <span>{{ user.role | uppercase }}</span>
      <button (click)="onSelect()">Select User</button>
    </div>
  `,
  styles: [`
    .card { border: 1px solid #e2e8f0; padding: 1rem; border-radius: 8px; }
  `]
})
export class UserCardComponent implements OnInit {
  @Input({ required: true }) user!: User;
  @Output() userSelected = new EventEmitter<User>();

  ngOnInit(): void {
    console.log('Component initialized for user:', this.user.name);
  }

  onSelect(): void {
    this.userSelected.emit(this.user);
  }
}

Data Binding: Four Types

Angular supports four types of data binding that connect the component class to the template. Interpolation ({{ }}) renders class properties as text. Property binding ([property]="expression") sets DOM or component properties. Event binding ((event)="handler()") responds to user actions. Two-way binding ([(ngModel)]="property") combines property and event binding to keep the view and model in sync — this requires importing FormsModule.

<!-- app.component.html -->

<!-- 1. Interpolation: renders text -->
<h1>{{ title }}</h1>
<p>Hello, {{ user.name }}!</p>

<!-- 2. Property binding: sets DOM property -->
<img [src]="user.avatarUrl" [alt]="user.name">
<button [disabled]="isLoading">Submit</button>
<app-user-card [user]="currentUser"></app-user-card>

<!-- 3. Event binding: responds to DOM events -->
<button (click)="handleClick()">Click Me</button>
<input (input)="onInputChange($event)">
<form (ngSubmit)="onSubmit()">...</form>

<!-- 4. Two-way binding: syncs view and model -->
<input [(ngModel)]="searchTerm" placeholder="Search...">
<p>You typed: {{ searchTerm }}</p>

<!-- Template reference variable (#) -->
<input #emailInput type="email">
<button (click)="handleEmail(emailInput.value)">Submit</button>

Component Communication: @Input and @Output

Parent and child components communicate through @Input and @Output decorators. @Input allows a parent to pass data down to a child component via property binding. @Output paired with EventEmitter allows a child to emit events that the parent can listen to. This creates a clear, unidirectional data flow pattern that is easy to test and reason about.

// parent.component.ts
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [UserCardComponent, NgFor],
  template: `
    <h2>Users ({{ selectedCount }} selected)</h2>
    <app-user-card
      *ngFor="let user of users"
      [user]="user"
      (userSelected)="onUserSelected($event)">
    </app-user-card>
  `
})
export class ParentComponent {
  users: User[] = [...];
  selectedCount = 0;

  onUserSelected(user: User): void {
    this.selectedCount++;
    console.log('Selected:', user.name);
  }
}

Component Lifecycle Hooks

Angular components have a defined lifecycle managed by Angular. The key hooks are: ngOnChanges (called when input properties change), ngOnInit (called once after first ngOnChanges — use for initialization logic), ngDoCheck (custom change detection), ngAfterContentInit (after content projection), ngAfterViewInit (after view and child views are initialized), and ngOnDestroy (cleanup before component is destroyed — unsubscribe here to prevent memory leaks).

import {
  Component, OnInit, OnDestroy, OnChanges,
  Input, SimpleChanges
} from '@angular/core';
import { Subscription } from 'rxjs';

@Component({ selector: 'app-lifecycle', template: '' })
export class LifecycleComponent implements OnInit, OnChanges, OnDestroy {
  @Input() userId!: number;
  private subscription = new Subscription();

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['userId'] && !changes['userId'].firstChange) {
      console.log('userId changed to:', changes['userId'].currentValue);
      this.loadUser(changes['userId'].currentValue);
    }
  }

  ngOnInit(): void {
    // Safe to access @Input values here
    this.loadUser(this.userId);
  }

  private loadUser(id: number): void { /* ... */ }

  ngOnDestroy(): void {
    // Cleanup: unsubscribe to prevent memory leaks
    this.subscription.unsubscribe();
  }
}

Angular Directives: Extending HTML

Directives are classes that add behavior to DOM elements. Angular has three types: components (directives with templates), structural directives (change DOM layout by adding/removing elements), and attribute directives (change appearance or behavior of an element). Built-in directives cover the most common use cases, but you can create custom directives for reusable behavior.

Structural Directives: ngIf, ngFor, ngSwitch

Structural directives manipulate the DOM structure. The asterisk (*) prefix is syntactic sugar that Angular expands into a longer form. *ngIf conditionally includes/excludes elements, *ngFor iterates over a collection and stamps out a template for each item, and *ngSwitch switches among a set of views based on a condition. In Angular 17+, the new control flow syntax (@if, @for, @switch) replaces these with a more readable, type-safe alternative.

<!-- ngIf: conditional rendering -->
<div *ngIf="isLoggedIn; else loginBlock">
  <p>Welcome back, {{ user.name }}!</p>
</div>
<ng-template #loginBlock>
  <app-login-form></app-login-form>
</ng-template>

<!-- ngFor: list rendering with index and trackBy -->
<ul>
  <li *ngFor="let item of items; let i = index; trackBy: trackById">
    {{ i + 1 }}. {{ item.name }}
  </li>
</ul>

<!-- Angular 17+ new control flow syntax -->
@if (isLoggedIn) {
  <p>Welcome, {{ user.name }}!</p>
} @else {
  <app-login-form />
}

@for (item of items; track item.id; let i = $index) {
  <li>{{ i + 1 }}. {{ item.name }}</li>
} @empty {
  <li>No items found.</li>
}

@switch (user.role) {
  @case ('admin') { <app-admin-panel /> }
  @case ('editor') { <app-editor-panel /> }
  @default { <app-viewer-panel /> }
}

Attribute Directives: ngClass, ngStyle, Custom

Attribute directives modify the appearance or behavior of elements without changing the DOM structure. ngClass dynamically adds or removes CSS classes, ngStyle dynamically sets inline styles. Custom attribute directives use the @Directive decorator and can access the host element via ElementRef and listen to host events with @HostListener.

<!-- ngClass: dynamic CSS classes -->
<div [ngClass]="{
  'active': isActive,
  'error': hasError,
  'loading': isLoading
}">
  Status indicator
</div>

<!-- ngStyle: dynamic inline styles -->
<div [ngStyle]="{
  'color': textColor,
  'font-size': fontSize + 'px',
  'background-color': isHighlighted ? '#fef3c7' : 'transparent'
}">
  Styled content
</div>

<!-- Built-in pipes -->
<p>{{ price | currency:'USD' }}</p>
<p>{{ date | date:'longDate' }}</p>
<p>{{ name | uppercase }}</p>
<p>{{ longText | slice:0:100 }}...</p>

Creating Custom Directives

Custom directives are created with the @Directive decorator. They receive the host ElementRef for direct DOM manipulation and can accept @Input properties to configure behavior. The @HostListener decorator binds to host element events. The @HostBinding decorator binds to host element properties or attributes. Custom directives are powerful for reusable behaviors like auto-focus, highlight on hover, drag-and-drop, and input masking.

// highlight.directive.ts
import {
  Directive, ElementRef, HostListener,
  Input, OnInit
} from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective implements OnInit {
  @Input() appHighlight = '#fef3c7';
  @Input() defaultColor = 'transparent';

  constructor(private el: ElementRef) {}

  ngOnInit(): void {
    this.el.nativeElement.style.transition = 'background-color 0.2s';
  }

  @HostListener('mouseenter')
  onMouseEnter(): void {
    this.el.nativeElement.style.backgroundColor = this.appHighlight;
  }

  @HostListener('mouseleave')
  onMouseLeave(): void {
    this.el.nativeElement.style.backgroundColor = this.defaultColor;
  }
}

// Usage in template:
// <p appHighlight="#d1fae5">Hover over me!</p>
// <p [appHighlight]="userColor" defaultColor="#f0f9ff">Custom color</p>

Services and Dependency Injection

Services are singleton classes in Angular that encapsulate business logic, data access, and shared functionality. They follow the Single Responsibility Principle: components handle UI, services handle everything else. The Angular dependency injection (DI) system automatically provides service instances to classes that need them via constructor injection.

@Injectable and the DI System

Services are decorated with @Injectable. The providedIn property determines the injection scope. providedIn: "root" creates a singleton available application-wide (the most common choice). providedIn: "any" creates a separate instance per lazy-loaded module. Providing in a specific module creates a scoped singleton. Angular 14+ supports the inject() function as an alternative to constructor injection, enabling dependency injection in standalone functions.

// auth.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs';

interface AuthUser {
  id: number;
  email: string;
  token: string;
}

@Injectable({
  providedIn: 'root', // singleton for entire app
})
export class AuthService {
  private http = inject(HttpClient);
  private currentUserSubject = new BehaviorSubject<AuthUser | null>(null);

  // Expose as read-only Observable
  currentUser$ = this.currentUserSubject.asObservable();

  get isLoggedIn(): boolean {
    return this.currentUserSubject.value !== null;
  }

  login(email: string, password: string): Observable<AuthUser> {
    return this.http.post<AuthUser>('/api/auth/login', { email, password }).pipe(
      tap(user => {
        localStorage.setItem('token', user.token);
        this.currentUserSubject.next(user);
      })
    );
  }

  logout(): void {
    localStorage.removeItem('token');
    this.currentUserSubject.next(null);
  }
}

HttpClient: Making API Calls

Angular provides HttpClient in @angular/common/http for making HTTP requests. It returns Observables (not Promises), integrates with Angular change detection, supports interceptors for cross-cutting concerns (auth headers, error handling, logging), and provides type-safe responses with generics. Import HttpClientModule in AppModule or use provideHttpClient() in standalone bootstrapping.

// products.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

interface ProductsResponse {
  data: Product[];
  total: number;
  page: number;
}

@Injectable({ providedIn: 'root' })
export class ProductsService {
  private http = inject(HttpClient);
  private baseUrl = '/api/products';

  getProducts(page = 1, category?: string): Observable<ProductsResponse> {
    let params = new HttpParams().set('page', page);
    if (category) params = params.set('category', category);

    return this.http.get<ProductsResponse>(this.baseUrl, { params });
  }

  getProduct(id: number): Observable<Product> {
    return this.http.get<Product>(this.baseUrl + '/' + id);
  }

  createProduct(product: Omit<Product, 'id'>): Observable<Product> {
    return this.http.post<Product>(this.baseUrl, product);
  }

  updateProduct(id: number, updates: Partial<Product>): Observable<Product> {
    return this.http.patch<Product>(this.baseUrl + '/' + id, updates);
  }

  deleteProduct(id: number): Observable<void> {
    return this.http.delete<void>(this.baseUrl + '/' + id);
  }
}

// HTTP Interceptor (Angular 15+ functional style)
// app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';

export const authInterceptor = (req, next) => {
  const token = localStorage.getItem('token');
  if (token) {
    req = req.clone({
      setHeaders: { Authorization: 'Bearer ' + token }
    });
  }
  return next(req);
};

// Bootstrap:
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
});

Reactive Forms and Template-Driven Forms

Angular provides two approaches to building forms. Template-driven forms use NgModel directives in the template and are simpler for basic use cases but harder to test and validate. Reactive forms (also called model-driven forms) define the form structure in the component class using FormGroup and FormControl — they are the recommended approach for any form with complex validation, dynamic fields, or that needs to be unit tested.

FormGroup and FormControl

A reactive form is built from a tree of FormGroup and FormControl instances. FormControl tracks the value and validation status of an individual input. FormGroup tracks the value and validity of a group of controls as a whole. FormArray manages an array of controls. The FormBuilder service provides convenient shorthand syntax for creating these programmatically. Each control tracks its dirty, pristine, touched, untouched, valid, and invalid states.

// registration.component.ts
import { Component, inject } from '@angular/core';
import {
  FormBuilder, FormGroup, FormControl,
  Validators, ReactiveFormsModule
} from '@angular/forms';

@Component({
  selector: 'app-registration',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div>
        <label>Email</label>
        <input formControlName="email" type="email">
        @if (email.invalid && email.touched) {
          @if (email.errors?.['required']) { <span>Email is required</span> }
          @if (email.errors?.['email']) { <span>Invalid email format</span> }
        }
      </div>
      <div formGroupName="password">
        <input formControlName="value" type="password">
        <input formControlName="confirm" type="password">
        @if (form.get('password')?.errors?.['mismatch']) {
          <span>Passwords do not match</span>
        }
      </div>
      <button type="submit" [disabled]="form.invalid">Register</button>
    </form>
  `
})
export class RegistrationComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    name: ['', [Validators.required, Validators.minLength(2)]],
    password: this.fb.group({
      value: ['', [Validators.required, Validators.minLength(8)]],
      confirm: ['', Validators.required],
    }, { validators: passwordMatchValidator }),
  });

  get email() { return this.form.get('email') as FormControl; }

  onSubmit(): void {
    if (this.form.valid) {
      console.log(this.form.value);
    }
  }
}

// Custom cross-field validator
function passwordMatchValidator(group: AbstractControl) {
  const value = group.get('value')?.value;
  const confirm = group.get('confirm')?.value;
  return value === confirm ? null : { mismatch: true };
}

RxJS and Observables in Angular

RxJS (Reactive Extensions for JavaScript) is deeply integrated into Angular. HTTP requests, router events, form value changes, and the async pipe all use Observables. Understanding RxJS is essential for effective Angular development. Observables represent a stream of values over time and are lazy (nothing happens until you subscribe), cancelable (unlike Promises), and composable with operators.

The async Pipe: Automatic Subscription Management

The async pipe is one of Angular's most powerful features. It subscribes to an Observable or Promise in the template, automatically unsubscribes when the component is destroyed (preventing memory leaks), and triggers change detection when new values arrive. The pattern of exposing data as Observables from services and using async pipe in templates is the recommended Angular pattern — it keeps components lean and handles subscription lifecycle automatically.

// products-list.component.ts
import { Component, inject } from '@angular/core';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { ProductsService } from '../services/products.service';

@Component({
  selector: 'app-products-list',
  standalone: true,
  imports: [AsyncPipe, NgFor, NgIf],
  template: `
    @if (products$ | async; as response) {
      <p>Total: {{ response.total }}</p>
      @for (product of response.data; track product.id) {
        <div>{{ product.name }} - {{ product.price | currency }}</div>
      }
    } @else {
      <app-loading-spinner />
    }
  `
})
export class ProductsListComponent {
  private productService = inject(ProductsService);

  // No manual subscribe/unsubscribe needed!
  products$ = this.productService.getProducts();
}

Essential RxJS Operators

RxJS provides 100+ operators, but a few cover 90% of use cases. map transforms each emitted value. filter discards values that do not match a predicate. switchMap cancels the previous inner Observable and subscribes to a new one (ideal for search autocomplete: cancel previous HTTP request when new input arrives). mergeMap subscribes to inner Observables concurrently. combineLatest emits when any source emits, combining latest values. debounceTime delays emissions by a specified time, ignoring intermediate values (ideal for search inputs to avoid firing on every keystroke).

// search.component.ts — debounced search with switchMap
import { Component, inject, OnInit } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import {
  debounceTime, distinctUntilChanged,
  switchMap, filter, map, catchError
} from 'rxjs/operators';
import { of } from 'rxjs';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [ReactiveFormsModule, AsyncPipe, NgFor],
  template: `
    <input [formControl]="searchControl" placeholder="Search products...">
    @for (result of results$ | async; track result.id) {
      <div>{{ result.name }}</div>
    }
  `
})
export class SearchComponent {
  private http = inject(HttpClient);
  searchControl = new FormControl('');

  results$ = this.searchControl.valueChanges.pipe(
    debounceTime(300),          // Wait 300ms after last keystroke
    distinctUntilChanged(),     // Skip if same value
    filter(term => term!.length >= 2), // Min 2 chars
    switchMap(term =>           // Cancel previous request
      this.http.get<Product[]>('/api/search?q=' + term).pipe(
        catchError(() => of([]))  // Handle errors gracefully
      )
    ),
    map(results => results.slice(0, 10)) // Top 10 results
  );
}

Subjects and BehaviorSubjects for State

A Subject is both an Observable and an Observer — it can emit values and be subscribed to. A BehaviorSubject holds a current value and emits it immediately to new subscribers (ideal for current user state, theme settings). A ReplaySubject buffers a specified number of emissions and replays them to new subscribers. In services, expose Subjects as Observables (using .asObservable()) to prevent external code from calling next() directly.

// theme.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Subject, ReplaySubject } from 'rxjs';

type Theme = 'light' | 'dark';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  // BehaviorSubject: has initial value, emits immediately to new subscribers
  private themeSubject = new BehaviorSubject<Theme>('light');
  theme$ = this.themeSubject.asObservable(); // read-only

  // Subject: no initial value, emits to current subscribers only
  private notificationSubject = new Subject<string>();
  notification$ = this.notificationSubject.asObservable();

  // ReplaySubject: buffers last N emissions for new subscribers
  private activityLog = new ReplaySubject<string>(5); // last 5 actions
  activityLog$ = this.activityLog.asObservable();

  get currentTheme(): Theme {
    return this.themeSubject.value;
  }

  toggleTheme(): void {
    const next: Theme = this.themeSubject.value === 'light' ? 'dark' : 'light';
    this.themeSubject.next(next);
    this.activityLog.next('Theme changed to ' + next);
  }

  notify(message: string): void {
    this.notificationSubject.next(message);
  }
}

Angular Router: Navigation and Lazy Loading

The Angular Router enables navigation between views in a single-page application. Routes map URL paths to components. The router handles browser history, URL parameters, query parameters, route guards, and lazy loading of feature modules. Configure routes with RouterModule.forRoot() at the application level and RouterModule.forChild() in feature modules.

Lazy Loading: Performance at Scale

Lazy loading splits your application into separate JavaScript bundles loaded on demand when the user navigates to that route. This dramatically reduces initial bundle size and time-to-interactive. In Angular 17+, lazy loading is configured with loadComponent (for standalone components) or loadChildren (for route configuration files). The Angular CLI automatically handles code splitting. Preloading strategies (PreloadAllModules) can pre-load lazy modules in the background after the initial load.

// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./home/home.component')
      .then(m => m.HomeComponent)
  },
  {
    path: 'products',
    loadComponent: () => import('./products/products-list.component')
      .then(m => m.ProductsListComponent)
  },
  {
    path: 'products/:id',
    loadComponent: () => import('./products/product-detail.component')
      .then(m => m.ProductDetailComponent)
  },
  {
    path: 'admin',
    canActivate: [authGuard],
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.adminRoutes)
  },
  {
    path: '**',
    loadComponent: () => import('./not-found/not-found.component')
      .then(m => m.NotFoundComponent)
  }
];

// app.config.ts
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';

export const appConfig = {
  providers: [
    provideRouter(routes, withPreloading(PreloadAllModules))
  ]
};

Route Guards: Protecting Routes

Route guards control navigation to and from routes. CanActivate prevents unauthorized navigation to a route (e.g., authentication check). CanDeactivate prevents leaving a route with unsaved changes. CanActivateChild guards child routes. Resolve pre-fetches data before a route activates, eliminating loading states in the component. In Angular 15+, guards are functions (not classes) using the inject() function for dependencies, making them simpler to write and test.

// auth.guard.ts (Angular 15+ functional style)
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isLoggedIn) {
    return true;
  }

  // Redirect to login, preserve intended URL
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};

// Unsaved changes guard
export const unsavedChangesGuard: CanDeactivateFn<EditFormComponent> =
  (component) => {
    if (component.hasUnsavedChanges()) {
      return window.confirm('You have unsaved changes. Leave anyway?');
    }
    return true;
  };

ActivatedRoute: Reading URL Parameters

ActivatedRoute provides access to route information: URL parameters (route.params or route.paramMap), query parameters (route.queryParams or route.queryParamMap), route data (route.data), and URL segments (route.url). All are exposed as Observables — subscribe to handle navigation between the same component with different parameters. Use snapshot for one-time access when you only need the initial value.

// product-detail.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { switchMap } from 'rxjs/operators';

@Component({ selector: 'app-product-detail', standalone: true, template: '' })
export class ProductDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);
  private router = inject(Router);
  private productService = inject(ProductsService);

  // Reactive: re-fetches when :id changes without re-creating the component
  product$ = this.route.paramMap.pipe(
    map(params => Number(params.get('id'))),
    switchMap(id => this.productService.getProduct(id))
  );

  ngOnInit(): void {
    // Query params
    this.route.queryParams.subscribe(params => {
      const tab = params['tab'] || 'details';
      console.log('Active tab:', tab);
    });
  }

  goBack(): void {
    this.router.navigate(['/products']);
  }

  goToEdit(id: number): void {
    this.router.navigate(['/products', id, 'edit'], {
      queryParams: { mode: 'full' }
    });
  }
}

State Management: NgRx and the Store Pattern

For complex applications, component @Input/@Output communication and services with Subjects become difficult to manage. NgRx (inspired by Redux) provides a predictable state container: a single immutable store, actions that describe state changes, reducers that compute new state from actions, selectors that derive data from the store, and effects that handle side effects (HTTP calls). This pattern makes state changes explicit, traceable, and testable.

NgRx Actions and Reducers

Actions are simple objects with a type string describing what happened (e.g., "[Product List] Load Products", "[Cart] Add Item"). They can carry a payload. The createAction function creates type-safe action creators. Reducers are pure functions that take the current state and an action and return a new state — never mutate the existing state. The createReducer function with on() handles specific actions. The initial state defines the state shape and default values.

// store/products/products.actions.ts
import { createAction, props } from '@ngrx/store';

// Load actions
export const loadProducts = createAction('[Product List] Load Products');
export const loadProductsSuccess = createAction(
  '[Product List] Load Products Success',
  props<{ products: Product[] }>()
);
export const loadProductsFailure = createAction(
  '[Product List] Load Products Failure',
  props<{ error: string }>()
);

// CRUD actions
export const addToCart = createAction(
  '[Cart] Add Item',
  props<{ product: Product; quantity: number }>()
);

// store/products/products.reducer.ts
import { createReducer, on } from '@ngrx/store';

interface ProductsState {
  products: Product[];
  loading: boolean;
  error: string | null;
}

const initialState: ProductsState = {
  products: [],
  loading: false,
  error: null,
};

export const productsReducer = createReducer(
  initialState,
  on(loadProducts, state => ({ ...state, loading: true, error: null })),
  on(loadProductsSuccess, (state, { products }) => ({
    ...state,
    loading: false,
    products,
  })),
  on(loadProductsFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error,
  }))
);

Selectors: Derived State

Selectors are pure functions that select, derive, and memoize state. createSelector combines multiple selectors and memoizes the result — the projector function only re-runs when input selectors produce new values. This makes selectors very efficient even in complex applications. Select state in components with store.select(mySelector), which returns an Observable that emits when the selected state changes. Compose selectors for maximum reuse.

// store/products/products.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';

const selectProductsState = createFeatureSelector<ProductsState>('products');

export const selectAllProducts = createSelector(
  selectProductsState,
  state => state.products
);

export const selectProductsLoading = createSelector(
  selectProductsState,
  state => state.loading
);

// Derived selector: memoized, only recalculates when inputs change
export const selectAvailableProducts = createSelector(
  selectAllProducts,
  products => products.filter(p => p.stock > 0)
);

export const selectProductById = (id: number) => createSelector(
  selectAllProducts,
  products => products.find(p => p.id === id)
);

// Using in component:
@Component({ template: '' })
export class ProductsComponent {
  private store = inject(Store);

  products$ = this.store.select(selectAvailableProducts);
  loading$ = this.store.select(selectProductsLoading);

  ngOnInit() {
    this.store.dispatch(loadProducts());
  }
}

NgRx Effects: Side Effects

Effects handle asynchronous side effects (HTTP calls, localStorage, timers) triggered by actions. An effect listens for specific actions using Actions and ofType(), performs the side effect, and dispatches a success or failure action. Effects keep reducers pure and components free from async logic. The createEffect function wraps the effect Observable and handles error recovery with catchError to prevent the effect from dying on error.

// store/products/products.effects.ts
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { switchMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

@Injectable()
export class ProductsEffects {
  private actions$ = inject(Actions);
  private productService = inject(ProductsService);

  loadProducts$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadProducts),
      switchMap(() =>
        this.productService.getProducts().pipe(
          map(response => loadProductsSuccess({ products: response.data })),
          catchError(error =>
            of(loadProductsFailure({ error: error.message }))
          )
        )
      )
    )
  );

  // Effect that dispatches no action
  logProductLoad$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadProductsSuccess),
      tap(({ products }) => console.log('Loaded ' + products.length + ' products'))
    ),
    { dispatch: false }
  );
}

Angular vs React vs Vue: Framework Comparison

Choosing a frontend framework depends on your team size, project complexity, and organizational needs. Here is a detailed comparison of Angular, React, and Vue across critical dimensions.

DimensionAngularReactVue
TypeFull frameworkUI libraryProgressive framework
LanguageTypeScript (mandatory)JS or TypeScriptJS or TypeScript
Maintained byGoogleMeta (Facebook)Community / Evan You
Learning curveSteep (DI, RxJS, decorators)Moderate (hooks, JSX)Gentle (Options/Composition API)
RoutingBuilt-in (Angular Router)Third-party (React Router)Built-in (Vue Router)
State managementServices/NgRx/SignalsRedux/Zustand/RecoilVuex / Pinia
FormsBuilt-in (reactive + template)Third-party (React Hook Form)Built-in (v-model)
HTTP clientBuilt-in (HttpClient)Third-party (Axios/fetch)Third-party (Axios/fetch)
Render approachIncremental DOM / Zone.jsVirtual DOMVirtual DOM (VDOM)
Min bundle (gzip)~30KB (standalone)~45KB (React + ReactDOM)~16KB (Vue 3)
Enterprise adoptionVery high (banking, gov, Google)Very high (startups + enterprise)High (Asia, mid-market)
TestingTestBed (built-in DI testing)React Testing LibraryVue Testing Library
SSR supportAngular Universal (built-in)Next.jsNuxt.js
Best forLarge enterprise apps, big teamsAny scale, flexible architectureRapid development, gentle onboarding

Angular 17+ New Features

Angular 17 (released November 2023) introduced the most significant developer experience improvements since Angular 2. The new control flow syntax (@if, @for, @switch) built into the template compiler replaces *ngIf and *ngFor with better performance and type safety. Standalone components are now the default — NgModule is optional. Signals provide a new, fine-grained reactivity primitive that integrates with Angular change detection. The new application builder (Vite + esbuild) delivers 87% faster build times.

Angular Signals: Fine-Grained Reactivity

Signals (Angular 16+) are reactive primitives that track dependencies automatically. A signal holds a value and notifies consumers when it changes. signal() creates a writable signal, computed() creates a derived signal (like Vue's computed), and effect() runs side effects when signals change. Signals integrate with Angular's change detection, enabling fine-grained updates without Zone.js in future versions. They are simpler to reason about than RxJS for synchronous state.

// counter.component.ts — Signals example
import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double: {{ doubled() }}</p>
    <button (click)="increment()">+</button>
    <button (click)="decrement()">-</button>
  `
})
export class CounterComponent {
  count = signal(0);

  // Computed signal: automatically updates when count changes
  doubled = computed(() => this.count() * 2);

  constructor() {
    // Effect: runs side effect when signal changes
    effect(() => {
      console.log('Count is now:', this.count());
      // Automatically tracks this.count() as a dependency
    });
  }

  increment(): void { this.count.update(v => v + 1); }
  decrement(): void { this.count.update(v => v - 1); }
}

// Signals from Observables
import { toSignal, toObservable } from '@angular/core/rxjs-interop';

@Component({ standalone: true, template: '' })
export class ExampleComponent {
  private userService = inject(UserService);

  // Convert Observable to Signal
  currentUser = toSignal(this.userService.currentUser$);

  // Use in template without async pipe
  // {{ currentUser()?.name }}
}

Standalone Components

Standalone components (stable in Angular 15, default in 17) do not require NgModule. The standalone: true flag in @Component allows direct importing of dependencies (other components, pipes, directives) in the imports array. bootstrapApplication() replaces platformBrowserDynamic().bootstrapModule() for the root application. Standalone APIs make Angular apps smaller (no module boilerplate), easier to understand, and better for tree shaking.

// main.ts — Angular 17+ bootstrap (no AppModule!)
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideStore } from '@ngrx/store';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor])),
    provideAnimations(),
    provideStore({ products: productsReducer }),
  ]
});

// app.component.ts — standalone root component
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    RouterOutlet,
    RouterLink,
    HeaderComponent,
    FooterComponent,
  ],
  template: `
    <app-header />
    <main>
      <router-outlet />
    </main>
    <app-footer />
  `
})
export class AppComponent {}

Frequently Asked Questions

Is Angular good for small projects?

Angular has a steeper initial setup compared to React or Vue, making it potentially overkill for small projects or prototypes. The CLI, TypeScript requirement, and architectural patterns add complexity upfront. However, for projects expected to grow, starting with Angular's structure prevents painful refactoring later. Standalone components in Angular 17 significantly reduce boilerplate. For truly small projects (landing pages, simple SPAs), React or Vue may be a better fit.

What is the difference between Angular and AngularJS?

AngularJS (Angular 1.x) and Angular (2+) are completely different frameworks that share only the name. AngularJS was released in 2010 and uses JavaScript, a scope-based model, and two-way data binding with dirty checking. Angular 2 was a complete rewrite in 2016, using TypeScript, a component-based architecture, unidirectional data flow, ahead-of-time compilation, and a completely different API. AngularJS reached end of life in December 2021. When people say "Angular" today, they always mean Angular 2+.

How does Angular change detection work?

Angular uses Zone.js to intercept asynchronous operations (setTimeout, HTTP calls, DOM events) and automatically trigger change detection. By default, Angular checks every component in the tree after any async operation. The OnPush change detection strategy optimizes this by only checking a component when its inputs change or an Observable emits via async pipe. Signals (Angular 16+) provide an even more granular reactivity model that can eventually replace Zone.js entirely (Angular is working toward "zoneless" mode).

What are standalone components in Angular 17?

Standalone components are components that do not belong to any NgModule. Introduced as experimental in Angular 14 and stable in Angular 15, they became the default in Angular 17. With standalone: true in @Component, you import dependencies directly in the component's imports array instead of declaring them in a module. This eliminates the verbose NgModule pattern, reduces boilerplate, improves tree shaking, and makes components more self-contained and reusable.

When should I use NgRx vs Angular services for state?

Use Angular services (with BehaviorSubject or Signals) for most applications. NgRx adds significant complexity (boilerplate, learning curve, additional dependencies) and is only worthwhile for large applications with complex shared state that multiple teams touch, state changes that need to be logged/debugged/time-traveled, or when the codebase needs to onboard many developers who benefit from predictable patterns. The NgRx team itself recommends starting without NgRx and adding it only when needed.

How do Angular Signals compare to RxJS?

Signals and RxJS serve different purposes and are complementary, not replacements. Signals are synchronous reactive primitives designed for component state — they are simpler to learn and use for UI state that does not involve async operations. RxJS is powerful for complex async flows, event streams, and multi-step transformations (debounce, retry, race conditions). The Angular team provides interop between them: toSignal() converts an Observable to a Signal, toObservable() does the reverse.

What is tree shaking in Angular and why does it matter?

Tree shaking is the process of eliminating dead code (unused exports) from the final bundle during build. Angular's Ivy compiler and the CLI's production build (using webpack or esbuild) perform aggressive tree shaking. Standalone components improve tree shaking over NgModules because they explicitly declare their dependencies, making it easier for the bundler to identify unused code. This results in smaller bundle sizes: a minimal standalone Angular app is now under 30KB gzipped.

What version of Angular should I use in 2024/2025?

Use Angular 17 or 18 for new projects. Angular 17 introduced the new template control flow, standalone-by-default, the new application builder (Vite + esbuild), and the revamped angular.dev documentation. Angular 18 added zoneless change detection (experimental), built-in internationalization improvements, and Material 3 components. Angular follows a 6-month major release cycle with LTS support for 18 months after release, so Angular 16 and 17 are both in LTS as of 2024.

𝕏 Twitterin LinkedIn
Cet article vous a-t-il aidé ?

Restez informé

Recevez des astuces dev et les nouveaux outils chaque semaine.

Pas de spam. Désabonnez-vous à tout moment.

Essayez ces outils associés

{ }JSON Formatter.*Regex TesterB→Base64 Encoder

Articles connexes

Guide complet React Hooks : useState, useEffect et Hooks personnalises

Maitrisez React Hooks avec des exemples pratiques. useState, useEffect, useContext, useReducer, useMemo, useCallback, hooks personnalises et hooks concurrents React 18+.

Promises JavaScript et Async/Await : Guide Complet

Maîtrisez les Promises et async/await : création, chaînage, Promise.all et gestion d'erreurs.

Guide complet des generiques TypeScript 2026 : des bases aux patterns avances

Maitrisez les generiques TypeScript : parametres de type, contraintes, types conditionnels, types mappes, types utilitaires et patterns concrets.