Angular
Junior
Quiz-Angular-Junior
Angular-Signals
Computed
Effect
Angular-Defer
Lazy-Loading
Rxjs
Switchmap
Combinelatest
Qcm-Angular-Junior
Reactive-Forms
Angular-17
📝
Entretien Angular Junior — Signals, RxJS et @defer
20 questions Angular 17 niveau intermédiaire : signals (computed, effect, toSignal), blocs @defer, lazy loading standalone, opérateurs RxJS (switchMap, combineLatest), intercepteurs HTTP et reactive forms.
Banque de révision : les 60 questions corrigées
Voici l'intégralité des 60 questions de « Entretien Angular Junior — Signals, RxJS et @defer », chacune avec sa bonne réponse et son explication. Le quiz interactif ci-dessus en tire une sélection aléatoire à chaque tentative — utilisez cette liste complète pour réviser.
Explication :
Depuis Angular 17, les composants standalone sont la recommandation officielle. Un composant standalone définit
standalone: true et importe directement les directives, pipes et autres composants dont il a besoin via imports: [], sans passer par un NgModule.
Les nouvelles applications créées avec
ng new utilisent les composants standalone par défaut.
Explication :
Les deux méthodes fonctionnent. La fonction
inject() (introduite dans Angular 14) est plus flexible car elle peut être utilisée dans des fonctions utilitaires ou des guards, pas seulement dans les constructeurs.
La fonction
inject() est souvent préférée dans les composants standalone car elle réduit le boilerplate.
Explication :
La nouvelle syntaxe
@for nécessite un paramètre track obligatoire pour optimiser le rendu : @for (item of items; track item.id) { ... }. On peut aussi utiliser @empty pour afficher un contenu quand le tableau est vide.
Contrairement à
*ngFor où trackBy était optionnel, track est maintenant obligatoire pour de meilleures performances.
Explication :
La nouvelle syntaxe
@for permet d'ajouter un bloc @empty qui s'affiche quand le tableau itéré est vide. Exemple : @for (item of items; track item.id) { ... } @empty { <p>Aucun élément</p> }.
Cette approche est plus lisible que d'utiliser
*ngIf séparément pour vérifier si le tableau est vide.
Explication :
Le bloc
@defer permet de charger paresseusement (lazy loading) des parties du template. On peut le déclencher avec différents événements : on viewport, on interaction, on idle, on timer, ou on hover.
Exemple :
@defer (on viewport) { <composant-lourd /> } @placeholder { <div>Chargement...</div> }
Explication :
La syntaxe correcte est
@defer (on interaction(button)) où "button" est une référence de template (ex: #button). On peut aussi utiliser on interaction sans paramètre pour n'importe quelle interaction.
Autres déclencheurs :
on viewport (visible à l'écran), on idle (quand le navigateur est inactif), on timer(5s) (après 5 secondes).
Explication :
Angular 17 a introduit des améliorations significatives de la détection de changement, notamment un algorithme plus intelligent qui ne vérifie que les parties de l'arbre des composants potentiellement affectées par un changement, améliorant ainsi les performances.
Ces améliorations sont automatiques, vous n'avez rien à changer dans votre code pour en bénéficier.
Explication :
Avec les composants standalone, on utilise
loadComponent dans la configuration de la route : { path: "route", loadComponent: () => import("./mon.component").then(m => m.MonComponent) }.
Cette approche remplace
loadChildren qui était utilisée pour les modules. Le lazy loading est automatique : le composant n'est chargé que quand la route est activée.
Explication :
La nouvelle syntaxe
@if utilise des blocs et supporte nativement @else if et @else, ce qui la rend plus lisible que *ngIf avec ses templates ng-template. Elle est également plus performante.
Exemple :
@if (score > 90) { Excellent } @else if (score > 70) { Bien } @else { À améliorer }
Explication :
Les pipes s'utilisent normalement dans les expressions à l'intérieur des blocs
@for. Cependant, il n'est pas recommandé d'utiliser des pipes dans la déclaration @for (item of items | filter) car cela pourrait causer des problèmes de performance.
Pour filtrer des données, préférez le faire dans le composant (via un getter ou un signal) plutôt que dans le template.
Explication :
Les signals (introduits dans Angular 16, maturité dans Angular 17) sont un système de réactivité granulaire qui permet à Angular de savoir précisément quelles parties du template dépendent de quelles données, optimisant ainsi la détection de changement.
Un signal se crée avec
signal(valeurInitiale), se lit avec monSignal(), et se modifie avec monSignal.set() ou monSignal.update().
Explication :
Un signal se crée avec la fonction
signal() importée depuis @angular/core. Dans le template, on appelle le signal comme une fonction : {{ count() }}. Angular détecte automatiquement les changements du signal.
Pour créer un signal dérivé (computed signal), utilisez
computed(() => count() * 2).
Explication :
Avec Zone.js, Angular doit vérifier tout l'arbre des composants quand quelque chose peut avoir changé. Avec les signals, Angular sait exactement quels composants dépendent de quelles données, permettant des mises à jour beaucoup plus ciblées et performantes.
Angular 17 continue de supporter Zone.js, mais les signals sont l'avenir de la réactivité dans Angular.
Explication :
Un signal se modifie avec
.set() pour une nouvelle valeur, ou .update() pour une mise à jour basée sur la valeur précédente. update reçoit une fonction qui prend la valeur actuelle et retourne la nouvelle valeur. update(prev => prev + 1) est particulièrement utile pour les compteurs ou les accumulateurs.
Explication :
Un computed signal se crée avec
computed(() => ...). Il dépend d'autres signals et se recalcule automatiquement quand ses dépendances changent. Il est en lecture seule et ne peut pas être modifié directement.
Exemple :
fullName = computed(() => `${firstName()} ${lastName()}`);
Explication :
La fonction
toSignal() (importée de @angular/core/rxjs-interop) convertit un Observable en signal. Elle peut prendre un objet d'options comme initialValue ou requireSync.
N'oubliez pas d'importer
toSignal depuis @angular/core/rxjs-interop : import { toSignal } from "@angular/core/rxjs-interop";
Explication :
La syntaxe des événements n'a pas changé avec les composants standalone. On continue d'utiliser
(nomEvenement)="handler($event)" dans le template parent. Le composant enfant définit toujours @Output() nomEvenement = new EventEmitter<Type>().
Vous pouvez maintenant aussi utiliser des outputs comme des signaux avec la nouvelle API
output() (expérimental dans Angular 17).
Explication :
Les composants standalone importent leurs dépendances directement dans le tableau
imports: [] du décorateur @Component. Exemple : @Component({ standalone: true, imports: [FormsModule, CommonModule] }).
Le
CommonModule est souvent importé pour avoir accès aux directives ngIf, ngFor, etc., bien qu'avec la nouvelle syntaxe, vous n'en ayez plus besoin.
Explication :
L'hydration (ou réhydratation) est le processus par lequel Angular prend le HTML généré côté serveur et le rend interactif côté client en attachant les gestionnaires d'événements et en initialisant les composants.
Angular 17 a amélioré l'hydration avec une meilleure gestion des erreurs et des performances accrues.
Explication :
La commande
ng generate service mon-service (ou ng g s mon-service) crée un nouveau service. Elle génère un fichier mon-service.service.ts avec un décorateur @Injectable({ providedIn: "root" }).
Utilisez
ng g s services/mon-service pour créer le service dans un dossier spécifique.
Explication :
La meilleure pratique est d'utiliser un guard
canActivate. Avec Angular 17+, les guards peuvent être des fonctions simples : canActivate: [() => inject(AuthService).isLoggedIn() || inject(Router).navigate(["/login"])].
Les guards fonctions sont plus légers que les guards classes et sont la recommandation actuelle.
Explication :
Le HMR (Hot Module Replacement) permet de remplacer les modules modifiés en cours d'exécution sans recharger toute la page, préservant ainsi l'état de l'application (formulaires remplis, etc.). Cela accélère le développement.
Le HMR fonctionne particulièrement bien pour les changements de styles et de templates.
Explication :
Dans les applications standalone,
app.config.ts exporte une fonction provideAppConfig() qui centralise les providers de l'application (routeur, HttpClient, etc.). Elle est utilisée dans main.ts avec provideRouter() et autres.
Exemple :
export const appConfig: ApplicationConfig = { providers: [provideRouter(routes), provideHttpClient()] };
Explication :
Dans une application standalone, on configure
provideHttpClient() dans app.config.ts (ou dans les providers du composant racine). Ensuite, on injecte HttpClient normalement dans n'importe quel composant ou service.
Pour les intercepteurs HTTP, utilisez
provideHttpClient(withInterceptors([monInterceptor])).
Explication :
provideRouter() est la façon moderne de configurer le routeur dans les applications standalone. Elle remplace RouterModule.forRoot() utilisée avec les NgModules. Elle prend un tableau de routes en paramètre.
Exemple :
providers: [provideRouter(routes, withComponentInputBinding(), withViewTransitions())]
Explication :
Dans une application standalone, on utilise
provideAnimations() (ou provideNoopAnimations() pour désactiver) dans le tableau des providers de app.config.ts. Cela remplace BrowserAnimationsModule.
N'oubliez pas d'importer
provideAnimations depuis @angular/platform-browser/animations.
Explication :
Depuis Angular 16, on peut marquer un
@Input() comme requis avec l'option required: true. Si le parent ne fournit pas cette entrée, Angular lèvera une erreur à la compilation ou à l'exécution.
Cette fonctionnalité aide à détecter les oublis d'entrées obligatoires et rend les composants plus robustes.
Explication :
La fonction
toObservable() (importée de @angular/core/rxjs-interop) convertit un signal en Observable. L'Observable émet chaque fois que le signal change.
Utile pour intégrer des signaux avec des APIs qui attendent des Observables (comme certaines bibliothèques ou async pipe).
Explication :
Angular 17+ intègre la View Transition API, une fonctionnalité du navigateur qui permet des animations fluides entre différents états de l'interface, notamment lors des changements de route. On l'active avec
withViewTransitions() dans provideRouter().
Cela permet des animations natives très performantes sans avoir à configurer manuellement des transitions complexes.
Explication :
La fonction
inject() peut être utilisée dans n'importe quel contexte (fonctions, guards, resolvers, services), pas seulement dans les constructeurs de classes. Cela rend le code plus flexible et permet des patterns d'injection plus avancés.
Exemple :
export const authGuard = () => { const auth = inject(AuthService); return auth.isLoggedIn(); }
Explication :
switchMap souscrit à un nouvel Observable et annule automatiquement la souscription précédente. Idéal pour les recherches en temps réel : si l'utilisateur tape rapidement, les requêtes précédentes sont annulées et seule la dernière est conservée.
Pattern classique :
this.searchTerm$.pipe(debounceTime(300), distinctUntilChanged(), switchMap(term => this.api.search(term))).subscribe().
Explication :
mergeMap souscrit immédiatement à chaque nouvel Observable, permettant l'exécution parallèle. concatMap attend que le premier Observable se complète avant de s'abonner au suivant (séquentiel, ordre garanti).
Pour des opérations où l'ordre est important (ex: sauvegardes successives), utilisez
concatMap. Pour des opérations indépendantes (ex: suppression de plusieurs fichiers), utilisez mergeMap.
Explication :
Deux approches existent :
pipe(catchError(err => of(valeurDefaut))) pour transformer l'erreur en Observable, et le callback error dans subscribe({ next, error, complete }) pour réagir à l'erreur. La combinaison permet une gestion robuste.
Exemple :
this.http.get("/api").pipe(catchError(err => { this.showError(err); return EMPTY; })).subscribe(data => ...).
Explication :
Avec les reactive forms :
fb.group({ email: ["", [Validators.required, Validators.email]], password: ["", [Validators.required, Validators.minLength(8)]] }). Dans le template, [formGroup]="form", formControlName="email", et vérifier form.valid avant soumission.
Pour des validateurs asynchrones (ex: vérifier si l'email existe déjà), utilisez le troisième paramètre de FormControl :
new FormControl("", [Validators.required], [this.emailExistsValidator]).
Explication :
debounceTime(ms) attend que l'Observable soit "silencieux" pendant le délai spécifié avant d'émettre. Pour une recherche, si l'utilisateur tape rapidement, seule la valeur finale (après arrêt) est émise, évitant les requêtes inutiles.
Pattern classique :
valueChanges.pipe(debounceTime(300), distinctUntilChanged(), switchMap(term => search(term))).
Explication :
Une directive d'attribut se crée avec
@Directive({ selector: "[maDirective]" }). Pour modifier l'hôte, injectez ElementRef ou utilisez @HostBinding("style.color") color = "red" et @HostListener("mouseenter").
Exemple de directive de surbrillance :
@HostBinding("style.backgroundColor") bgColor = "transparent"; @HostListener("mouseenter") enter() { this.bgColor = "yellow"; }.
Explication :
Les trois approches sont complémentaires :
trackBy évite la recréation des éléments DOM qui n'ont pas changé ; OnPush évite de re-vérifier les composants enfants si leurs inputs n'ont pas changé ; le virtual scrolling (CDK) n'affiche que les éléments visibles pour les longues listes.
Pour les longues listes (>50 éléments), combinez
trackBy + OnPush + ScrollingModule du CDK Angular pour des performances optimales.
Explication :
BehaviorSubject<T> est un Observable spécial qui stocke la dernière valeur. Les nouveaux souscripteurs reçoivent immédiatement la valeur actuelle. On l'utilise dans les services pour partager l'état : private state = new BehaviorSubject<User|null>(null); readonly state$ = this.state.asObservable(); update(u: User) { this.state.next(u); }.
Exposez toujours un Observable (via
.asObservable()) publiquement et gardez le Subject privé pour éviter que les consommateurs ne puissent appeler .next().
Explication :
combineLatest([obs1$, obs2$, obs3$]) souscrit à tous les Observables et émet un tableau [val1, val2, val3] à chaque fois que l'un d'eux émet, en utilisant les dernières valeurs connues des autres. Il n'émet rien tant que chaque Observable n'a pas émis au moins une fois.
Idéal pour combiner des filtres :
combineLatest([search$, category$, sortOrder$]).pipe(switchMap(([search, cat, sort]) => api.getList(search, cat, sort))).
Explication :
effect(() => { console.log(count()); }) crée un effet qui s'exécute automatiquement quand les signals lus à l'intérieur changent. Contrairement aux computed signals, les effects sont utilisés pour les side-effects (logging, appels HTTP, manipulation du DOM).
Les effects sont détruits automatiquement quand le composant est détruit. Évitez de modifier des signals dans un effect pour prévenir les boucles infinies.
Explication :
Pour lazy-loader un ensemble de routes, utilisez
loadChildren avec l'importation dynamique d'un fichier de routes standalone : { path: "feature", loadChildren: () => import("./feature/feature.routes").then(r => r.FEATURE_ROUTES) }.
Différence avec
loadComponent : loadComponent charge un seul composant standalone ; loadChildren charge un ensemble de routes (un fichier de routes ou un module).
Explication :
Angular 17+ recommande les intercepteurs fonctionnels :
export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = inject(AuthService).getToken(); return next(req.clone({ setHeaders: { Authorization: token } })); }; puis dans app.config.ts : provideHttpClient(withInterceptors([authInterceptor])).
Les intercepteurs fonctionnels sont plus simples et plus légers que les intercepteurs de classe. Ils peuvent utiliser
inject() pour accéder aux services.
Explication :
Les animations Angular se définissent dans le décorateur
@Component via animations: [trigger("nomAnimation", [state(), transition(), animate()])]. On lie l'animation dans le template avec [@nomAnimation]="etat".
N'oubliez pas d'activer les animations :
provideAnimations() dans app.config.ts. Importez les fonctions d'animation depuis @angular/animations.
Explication :
Angular peut fonctionner avec une CSP stricte. En activant la politique CSP-Nonce dans
angular.json, le compilateur AOT génère des nonces uniques pour les styles inline, permettant d'interdire unsafe-inline dans la CSP.
Pour les tests, évitez les styles inline non nécessaires. Pour la production, configurez votre serveur web pour envoyer les en-têtes CSP appropriés.
Explication :
Ce pattern architectural sépare les responsabilités : les "smart components" (containers) gèrent l'état, les appels de services, et les données. Les "dumb components" (presentational) reçoivent des données via
@Input(), affichent et émettent des événements via @Output() sans accéder directement aux services.
Avantages : les dumb components sont plus faciles à tester, réutilisables et avec
OnPush ils offrent de meilleures performances.
Explication :
Avec
provideRouter(routes, withComponentInputBinding()), Angular lie automatiquement les paramètres de route, query params et données de route aux propriétés @Input() du composant routé. Plus besoin d'injecter ActivatedRoute pour les cas simples.
Exemple : route
{ path: "users/:id" } → @Input() id!: string; dans le composant. Angular injecte automatiquement la valeur du paramètre.
Explication :
Quand des données changent en dehors du cycle normal (setTimeout, WebSocket, callbacks), il faut notifier Angular manuellement.
ChangeDetectorRef.markForCheck() marque le composant comme devant être vérifié au prochain cycle. detectChanges() force une détection immédiate.
Injectez avec
private cdr = inject(ChangeDetectorRef) puis appelez this.cdr.markForCheck() après chaque mise à jour externe.
Explication :
Le CDK (Component Dev Kit) fournit des blocs de construction réutilisables : virtual scrolling pour les longues listes (
ScrollingModule), drag-and-drop (DragDropModule), overlays, portals, accessibilité (a11y), et plus. Il est utilisé en interne par Angular Material.
Le CDK est indépendant du design Angular Material : vous pouvez l'utiliser même sans Material pour accéder à ses fonctionnalités avancées.
Explication :
La nouvelle API
output() crée un OutputRef : protected deleted = output<number>();. Émission : this.deleted.emit(this.id). Le template parent reste identique : (deleted)="onDelete($event)". Plus lisible et cohérent avec les signals.
L'API
output() peut aussi créer des outputs depuis des Observables : outputFromObservable(this.mySubject$).
Explication :
Avec TestBed :
TestBed.configureTestingModule({ imports: [MonComposantStandalone] }), puis const fixture = TestBed.createComponent(MonComposant). fixture.detectChanges() lance la détection initiale. fixture.nativeElement donne accès au DOM rendu.
Pour les composants standalone, importez-les directement dans
imports au lieu de declarations.
Explication :
tap() permet d'effectuer des actions secondaires (side-effects) sans modifier le flux de données : obs$.pipe(tap(val => console.log("val:", val)), map(val => val * 2)). La valeur n'est pas modifiée par tap.
Utilisez
tap pour le débogage ou pour déclencher des actions comme afficher un spinner : tap(() => this.loading = true).
Explication :
On peut convertir les Observables de formulaires en signals :
formValue = toSignal(this.form.valueChanges, { initialValue: this.form.value }). Cela permet d'utiliser la valeur du formulaire dans des computed signals ou des effects.
Angular travaille sur une intégration plus profonde des formulaires avec les signals. En attendant,
toSignal(valueChanges) est le pont le plus pratique.
Explication :
La projection multi-slot permet de définir plusieurs points de projection dans un composant via des sélecteurs CSS :
<ng-content select="[header]">, <ng-content select="[body]">. Le parent projette dans les slots : <span header>Titre</span>.
Le sélecteur peut être un attribut (
select="[attr]"), une classe (select=".cls"), un tag (select="header") ou un composant (select="app-header").
Explication :
forkJoin([obs1$, obs2$, obs3$]) lance tous les Observables en parallèle et émet un seul tableau [res1, res2, res3] quand TOUS sont complétés. Si l'un échoue, tout échoue. Idéal pour charger plusieurs ressources indépendantes simultanément.
Exemple :
forkJoin({ user: http.get("/user"), settings: http.get("/settings") }).subscribe(({ user, settings }) => ...).
Explication :
Depuis Angular 14+, les resolvers peuvent être des fonctions :
{ path: "user/:id", component: UserComponent, resolve: { user: () => inject(UserService).getUser(inject(ActivatedRouteSnapshot).params["id"]) } }. Les données sont disponibles via ActivatedRoute.data.
Ou avec withComponentInputBinding(), le résolveur sera automatiquement injecté dans le @Input() correspondant :
@Input() user!: User;.
Explication :
shareReplay(1) rend le dernier résultat disponible immédiatement aux nouveaux souscripteurs, sans re-exécuter la requête source. Idéal pour les appels HTTP partagés entre plusieurs composants.
Pattern courant dans les services :
private config$ = this.http.get("/config").pipe(shareReplay(1)); — chaque composant qui souscrit obtient les données du cache, pas une nouvelle requête.
Explication :
Un validateur de groupe accède à tous les champs :
fb.group({ password: [""], confirm: [""] }, { validators: (g) => g.get("password")?.value === g.get("confirm")?.value ? null : { mismatch: true } }). L'erreur est sur le FormGroup, pas sur le FormControl.
Pour afficher l'erreur dans le template :
*ngIf="form.errors?.["mismatch"] && form.get("confirm")?.touched".
Explication :
Angular offre une solution i18n native (marqueurs
i18n + build AOT) performante car les traductions sont compilées. Pour plus de flexibilité, des bibliothèques comme ngx-translate permettent des changements de langue à chaud sans rebuild.
Pour les petits projets ou les changements de langue en temps réel, préférez
ngx-translate. Pour les grandes applications à performances critiques, préférez l'i18n natif d'Angular.
Explication :
L'accessibilité nécessite : HTML sémantique, attributs ARIA (
aria-label, aria-hidden, role), gestion du focus (@angular/cdk/a11y avec FocusTrap, LiveAnnouncer), et navigation clavier. Angular ne gère pas automatiquement l'a11y.
Utilisez
@angular/cdk/a11y pour les composants interactifs : FocusMonitor, FocusTrap, et LiveAnnouncer pour les annonces aux lecteurs d'écran.
Explication :
Le déclencheur
when permet de charger le contenu défèré quand une expression devient true : @defer (when isLoggedIn) { <profil-section /> } @placeholder { <skeleton /> }. Il surveille l'expression à chaque cycle de détection.
Les blocs @defer supportent plusieurs déclencheurs :
@defer (on viewport; when hasPermission) — charge quand l'élément est visible ET la condition est vraie.