Formation Angular moderne : signals, NgRx et tests

NgRx et state management Angular : état, formulaires et sécurité

Objectif : structurer l’état global proprement, concevoir des formulaires avancés et sécuriser l’application avec JWT, Guards et bonnes pratiques modernes…

Leçon gratuite

Objectifs pédagogiques du Jour 3

Ce module consolide la partie “mécanique” d’une application professionnelle : comment l’état est stocké, comment l’utilisateur interagit via les formulaires, et comment l’accès est sécurisé.

Résultats attendus

À la fin du jour 3, vous serez capable de :

  • Choisir une stratégie de gestion d’état adaptée à votre contexte (Signals, RxJS, NgRx).
  • Concevoir des formulaires complexes, robustes et orientés UX.
  • Mettre en place une authentification JWT sécurisée côté Angular + Spring Boot.

Gains business & techniques

Impact sur vos projets

  • Réduction de la dette technique liée au “state bricolé partout”.
  • Moins de bugs fonctionnels sur les formulaires (validation cohérente, claire).
  • Sécurité renforcée sans complexifier inutilement le code.

1. Gestion d’État Réactive (RxJS, NgRx, Signals)

L’état mal géré est une source majeure de bugs et de régressions. L’objectif : arrêter de diffuser des BehaviorSubject partout et structurer un vrai state management.

Scénario simple

State local avec Signals (Angular moderne)

Idéal pour une feature isolée (catalogue, panier, écran spécifique) : lisible, local, facile à tester.

// Store local de panier basé sur Signals
@Injectable({ providedIn: 'root' })
export class CartStore {
  readonly items = signal<Product[]>([]);

  readonly total = computed(
    () => this.items().reduce((acc, p) => acc + p.price, 0)
  );

  add(product: Product) {
    this.items.update(items => [...items, product]);
  }

  clear() {
    this.items.set([]);
  }
}

Gains

  • Aucun subscribe() à gérer → moins de fuites mémoire.
  • Debug très simple : un seul point d’entrée pour l’état.
  • Plus lisible qu’un mélange Observables/Subjects improvisés.

Cas applicatif large

NgRx / Store global pour les gros projets

Pour les applications avec beaucoup d’écrans, de rôles et de règles métier, un Store global reste pertinent.

// Exemple d'action NgRx
export const addItemToCart = createAction(
  '[Cart] Add Item',
  props<{ product: Product }>()
);

// Exemple de reducer
const reducer = createReducer(
  initialState,
  on(addItemToCart, (state, { product }) => ({
    ...state,
    items: [...state.items, product]
  }))
);

Bon réflexe

exposez le store via un service “Facade” (ex : CartFacade) pour ne pas coupler vos composants directement à NgRx.

À éviter absolument

  • Un mélange ad hoc : un peu de Subject, un peu de store, un peu de localStorage…
  • Du state global pour tout (inclus des infos qui pourraient rester locales au composant).
  • Des selectors qui contiennent de la logique métier complexe (la sortir dans des services).

2. Formulaires Complexes & UX Pro

L’utilisateur peut pardonner une erreur d’affichage, beaucoup moins un formulaire qui se comporte mal. Ici, on structure des Reactive Forms robustes.

Structure

Formulaire de commande (formulaire imbriqué)

this.orderForm = this.fb.group({
  customer: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]],
  confirmEmail: ['', Validators.required],
  address: this.fb.group({
    street: ['', Validators.required],
    city: ['', Validators.required],
    zip: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]]
  }),
  acceptTerms: [false, Validators.requiredTrue]
}, { validators: emailsMatch }); // validator croisé posé sur le groupe

Astuce

regroupez les champs liés dans des FormGroup (adresse, infos de paiement, etc.), cela simplifie la validation et la lecture.

Validation avancée

Validation croisée & logique dynamique

import { AbstractControl, ValidationErrors } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

// Validator croisé (ex: confirmation d'email) — à poser sur le FormGroup
export function emailsMatch(group: AbstractControl): ValidationErrors | null {
  const email = group.get('email')?.value;
  const confirm = group.get('confirmEmail')?.value;
  return email && confirm && email !== confirm ? { emailsMismatch: true } : null;
}

// Écoute des changements (désabonnement automatique à la destruction du composant)
this.orderForm.valueChanges
  .pipe(takeUntilDestroyed(this.destroyRef))
  .subscribe(value => {
    // adapter dynamiquement les options de livraison, par ex.
  });

Bonnes pratiques

  • Externaliser les validators dans des fichiers dédiés.
  • Rendre les messages d’erreur explicites et humains.
  • Préférer Reactive Forms dès qu’il y a du métier ou des règles évolutives.

Astuce d’expert

Depuis Angular 14, les Reactive Forms sont typés. Injectez NonNullableFormBuilder (ou fb.nonNullable.group(...)) : form.getRawValue() renvoie alors un objet exactement typé ({ email: string; ... }), sans null parasite, que vous pouvez passer directement à votre service HTTP.

3. Sécurité & Authentification côté Angular

L’authentification ne se limite pas à “vérifier si un token existe”. Il s’agit de protéger les routes, filtrer les appels et gérer les rôles correctement.

HTTP

Injection du JWT via un HttpInterceptor

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';

// Intercepteur fonctionnel (Angular 15+, recommandé avec provideHttpClient)
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).getToken();
  if (!token) return next(req);
  const cloned = req.clone({
    setHeaders: { Authorization: `Bearer ${token}` }
  });
  return next(cloned);
};

// app.config.ts
// provideHttpClient(withInterceptors([authInterceptor]))

Astuce

centraliser ici tous les headers communs (langue, correlation-id, etc.) au lieu de répéter dans chaque service.

Routes

Protection des routes avec un Guard

import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';

// Guard fonctionnel (l'interface CanActivate est dépréciée depuis Angular 15.2)
export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  return auth.isLoggedIn() ? true : inject(Router).createUrlTree(['/login']);
};

// app.routes.ts
// { path: 'orders', canActivate: [authGuard], loadComponent: ... }

Pattern

Role-Based Access Control (RBAC) + Guards → règles de sécurité centralisées, simples à tester.

4. Authentification JWT côté Spring Boot

Spring Boot s’occupe de l’authentification, de la génération et de la validation du token. Angular consomme simplement l’API exposée.

Login

Endpoint d’authentification

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthService service;

    public AuthController(AuthService service) { // sans constructeur, le champ final ne compile pas
        this.service = service;
    }

    @PostMapping("/login")
    public ResponseEntity<TokenResponse> login(@Valid @RequestBody LoginRequest req) {
        return ResponseEntity.ok(service.authenticate(req));
    }
}

Bonnes pratiques

Points de vigilance côté backend

  • Limiter la durée de vie des tokens (access token court, refresh token dédié).
  • Ne jamais mettre d’information sensible en clair dans le JWT.
  • Protéger les endpoints d’admin par rôle et non uniquement par présence du token.

Important

si possible, préférer les cookies HttpOnly pour le stockage des tokens plutôt que localStorage (protection XSS).

Suivez votre progression. Créez un compte gratuit pour cocher vos leçons et reprendre là où vous en étiez.

Créer un compte gratuit