60 questions qui reviennent vraiment dans les entretiens Angular en 2026, classées par niveau : ce qu’un junior doit savoir, ce que l’on attend d’un développeur confirmé qui construit seul des fonctionnalités, et ce sur quoi l’on interroge un senior : les rouages internes, l’architecture et les compromis. Chaque réponse commence par ce qu’il faut dire en premier, puis ce qu’il faut ajouter pour montrer que vous comprenez, la question qui suit généralement et la réponse qui fait échouer l’entretien.
Comment utiliser cette page
- Répondez à voix haute avant d’ouvrir la réponse. Lire donne l’impression de savoir ; le dire, c’est ce que vous ferez en entretien.
- Commencez par la réponse courte, puis ajoutez une ou deux idées de « Puis allez plus loin ». Les recruteurs préfèrent une position claire à une longue liste.
- Marquez ce que vous savez avec « Je maîtrise ». C’est enregistré dans ce navigateur, et l’entretien simulé vous pose le reste en premier.
- Chaque réponse renvoie vers la leçon qui explique correctement le sujet.
Entretien simulé
Quelques questions aléatoires, une à la fois, avec la réponse masquée jusqu’à ce que vous la demandiez. À la fin, vous obtenez la liste des questions à revoir.
Répondez à voix haute, comme si quelqu’un vous écoutait. Révélez ensuite la réponse type et soyez honnête avec vous-même.
Junior : les fondamentaux
Pour un premier poste Angular, on n’attend pas que vous connaissiez les rouages internes, mais on attend que vous maîtrisiez sans hésiter les bases modernes : composants standalone, nouveau Control Flow, signaux, inputs et outputs, services avec inject(), routing et formulaires. Montrez que vous savez pourquoi, pas seulement comment.
J-01Qu’est-ce qu’un composant, et que signifie « standalone » ? Comment une application démarre-t-elle sans NgModule ? Composants
À dire en premier
Un composant est une classe TypeScript décorée avec
@Componentqui associe un template à un état et à un comportement, et s’utilise via son sélecteur. Standalone (le défaut) signifie qu’il déclare lui-même ses dépendances dansimports, donc aucun NgModule n’est nécessaire : l’application démarre avecbootstrapApplication(App, appConfig).Puis allez plus loin
- Vous n’écrivez plus
standalone: true— c’est le défaut. Chaque composant importe exactement ce que son template utilise (d’autres composants, des pipes,RouterLink), si bien que les dépendances sont explicites et compatibles avec le tree-shaking. - La configuration globale de l’application vit dans
app.config.ts, sous forme deproviders:provideRouter(routes),provideHttpClient(...)seulement si vous le configurez, etc. - Une ancienne bibliothèque qui n’exporte qu’un NgModule peut quand même aller directement dans les
importsd’un composant ; les providers du module passent parimportProvidersFrom().
import { bootstrapApplication } from '@angular/platform-browser'; import { App } from './app/app'; import { appConfig } from './app/app.config'; bootstrapApplication(App, appConfig).catch((err) => console.error(err));On vous demandera probablement ensuite
Où placez-vous un provider dont toute l’application a besoin ?
Signal d’alerte
Dit que chaque composant doit être déclaré dans un
AppModule, ou importeCommonModule« au cas où ».- Vous n’écrivez plus
J-02Quels types de data binding existe-t-il dans un template ? Templates
À dire en premier
L’interpolation
{{ value }}et le property binding[prop]="expr"vont du composant vers le DOM ; l’event binding(event)="handler($event)"va du DOM vers le composant ; le two-way binding[(value)]="sig"combine les deux.Puis allez plus loin
[disabled]lie une propriété du DOM ; pour ce qui n’existe que comme attribut (ARIA,colspan), on utilise[attr.aria-label].[(x)](« banana in a box ») est du sucre syntaxique pour[x]="v()"+(xChange)="v.set($event)". Avec un signal, on passe le signal lui-même, sans parenthèses.- Les expressions de template doivent être légères et sans effet de bord ; la lecture d’un signal comme
count()est acceptable, mais l’appel d’une méthode coûteuse s’exécute à chaque vérification.
<h2>{{ user().name }}</h2> <img [src]="user().avatarUrl" [attr.aria-label]="user().name" /> <button [disabled]="saving()" (click)="save()">Save</button> <input (input)="query.set($any($event.target).value)" /> <app-toggle [(checked)]="notifications" />On vous demandera probablement ensuite
Quelle est la différence entre un attribut et une propriété ?
Signal d’alerte
Pense que
[value]="x"etvalue="x"sont équivalents (le second est une chaîne statique), ou écrit[(checked)]="notifications()".J-03Comment écrire des conditions et des boucles dans un template, et pourquoi
@forexige-t-iltrack? TemplatesÀ dire en premier
Avec le Control Flow intégré :
@if/@else,@foravec@empty, et@switch/@case.trackest obligatoire : il indique à Angular comment identifier chaque élément, afin de réutiliser les nœuds DOM au lieu de recréer toute la liste.Puis allez plus loin
- Trackez par un identifiant unique et stable (
track user.id) ;$indexseulement pour les listes statiques ; tracker l’objet lui-même signifie que chaque nouvelle requête détruit et recrée le DOM. - Il fait partie du langage : aucun import de
NgIf/NgFor.@forexpose$index,$first,$last,$even,$odd,$count. @if (user(); as u)fournit un alias typé et non nul. Depuis la v21.1, plusieurs@casepeuvent partager un bloc, et@default never;(v21.2) transforme un cas d’union oublié en erreur de compilation.
@if (tasks().length) { <ul> @for (task of tasks(); track task.id; let i = $index) { <li>{{ i + 1 }}. {{ task.title }}</li> } </ul> } @else { <p>You have no tasks.</p> }On vous demandera probablement ensuite
Que devient le DOM si vous trackez par
$indexpuis triez la liste ?Signal d’alerte
Utilise encore
*ngIf/*ngFordans du nouveau code, ou ne sait pas expliquer à quoi serttrack.- Trackez par un identifiant unique et stable (
J-04Qu’est-ce que
@letet quand l’utiliseriez-vous ? TemplatesÀ dire en premier
@letdéclare une variable locale en lecture seule dans le template, visible dans le bloc courant et ses enfants. Je l’utilise pour nommer une longue expression employée plusieurs fois, ou pour déballer une valeur une seule fois, p. ex.@let user = user$ | async;.Puis allez plus loin
- Elle ne peut pas être réaffectée depuis le template ; elle est recalculée quand ce qu’elle lit change.
- Elle est pratique dans
@forpour une valeur propre à chaque élément (p. ex. un prix final après remise). - La vraie logique appartient toujours à la classe, sous forme de
computed();@letsert la lisibilité, pas les règles métier.
Signal d’alerte
Utilise
@letcomme variable mutable, ou y déplace de la logique métier complexe dans le template.J-05Comment ajouter une classe CSS ou un style de façon conditionnelle ? Templates
À dire en premier
Avec les bindings natifs :
[class.active]="isActive()"pour une classe,[class]="{ ... }"pour plusieurs, et[style.width.px]="width()"pour les styles. Aucun import n’est nécessaire, et les classes statiques sont combinées aux classes liées, pas écrasées.Puis allez plus loin
[class]accepte une chaîne, un tableau ou un objet ;[style]accepte une chaîne ou un objet, et les unités se placent dans le binding (.px,.%,.rem).[ngClass]/[ngStyle]sont l’ancienne méthode ; la v21 propose les migrationsngclass-to-classetngstyle-to-style.- Pour l’élément hôte du composant lui-même, on utilise les métadonnées
host('[class.is-loading]': 'loading()'), pas@HostBinding.
On vous demandera probablement ensuite
Comment définiriez-vous une variable CSS à partir d’un signal ?
Signal d’alerte
Construit les chaînes de classes à la main dans le composant, ou manipule directement
classListsur l’élément.J-06Qu’est-ce qu’une pipe, et que signifie « pure » ? Templates
À dire en premier
Une pipe transforme une valeur pour l’affichage, directement dans le template, comme
{{ price | currency }}. Par défaut elle est pure : Angular ne la réexécute que lorsque la valeur d’entrée ou ses arguments changent (par référence), elle est donc mémoïsée.Puis allez plus loin
- Les pipes sont standalone et s’importent comme n’importe quelle dépendance (
imports: [DatePipe]) ; une pipe personnalisée est une classe avec@Pipeettransform(). - Comme la vérification se fait par référence, une pipe pure ne se réexécute pas si vous mutez un tableau sur place.
- Une méthode appelée depuis le template s’exécute à chaque vérification ; pour les calculs propres à un composant, préférez
computed(), et réservez les pipes au formatage d’affichage réutilisable. Évitezpure: false.
On vous demandera probablement ensuite
Écririez-vous une pipe ou un
computed()pour filtrer une liste ?Signal d’alerte
Rend une pipe de filtrage impure pour « la faire marcher » sur une grande liste.
- Les pipes sont standalone et s’importent comme n’importe quelle dépendance (
J-07Comment un parent transmet-il des données à un composant enfant, et comment l’enfant répond-il ? Composants
À dire en premier
L’enfant déclare
input()pour les données entrantes etoutput()pour les événements sortants. Le parent lie[value]="..."et(rate)="onRate($event)"; l’enfant appellerate.emit(4).Puis allez plus loin
- Un
input()est un signal en lecture seule : on le lit avecvalue()et on en dérive aveccomputed()— pas besoin dengOnChanges. input.required<T>()transforme un binding manquant en erreur de compilation ;transform: numberAttribute/booleanAttributeconvertissentmax="10"ou un simple attributreadonlyvers le bon type.- L’enfant ne peut pas faire
set()sur son propre input. S’il doit changer la valeur, il émet un événement et laisse le parent décider, ou utilisemodel().
export class StarRating { readonly value = input.required<number>(); readonly max = input(5, { transform: numberAttribute }); readonly rate = output<number>(); protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1), ); }On vous demandera probablement ensuite
Qu’est-ce qui change si l’enfant doit modifier la valeur qu’il reçoit ?
Signal d’alerte
Dit qu’il faut
ngOnChangespour réagir à un input, ou utilise@Input/EventEmitterdans du nouveau code.- Un
J-08Qu’est-ce que
model()et en quoi diffère-t-il deinput()+output()? ComposantsÀ dire en premier
model()est un signal modifiable qui fait office d’input plus un outputxChangeautomatique, de sorte que le parent peut utiliser le two-way binding :[(checked)]="notifications". Quand l’enfant appelleset()ouupdate(), le signal du parent est mis à jour.Puis allez plus loin
- Utilisez
model()quand la valeur appartient vraiment aux deux côtés, comme dans un toggle, un stepper ou un champ personnalisé. - Utilisez
input()+output()quand l’enfant ne fait que proposer et que le parent décide (valide, confirme, enregistre côté serveur). - Dans un two-way binding, on passe le signal lui-même (
notifications), pas sa valeur (notifications()).
Signal d’alerte
Ne sait pas expliquer en quoi se développe
[( )], ou utilisemodel()pour tout.- Utilisez
J-09Qu’est-ce que la projection de contenu ? Composants
À dire en premier
Elle permet à un parent de placer son propre markup entre les balises d’un composant, et le composant décide où l’afficher avec
<ng-content />. Avecselect, on peut avoir plusieurs emplacements, par exemple un titre et des actions dans une carte.Puis allez plus loin
selectaccepte un sélecteur de balise, d’attribut ou de classe ([card-title]) ; le<ng-content />sansselectrécupère tout le reste, et vous pouvez y mettre un contenu de repli.- Piège : le contenu projeté est créé par le parent même si l’enfant le masque avec
@if. Pour un contenu vraiment lazy, passez un<ng-template>. - La projection convient au markup flexible (layouts, cartes, boîtes de dialogue) ; les inputs servent aux données que le composant met lui-même en forme.
On vous demandera probablement ensuite
Quand choisiriez-vous la projection plutôt qu’un input ?
Signal d’alerte
Passe du HTML dans un input de type chaîne et l’affiche avec
innerHTMLau lieu de le projeter.J-10Que sont
signal()etcomputed()? SignalsÀ dire en premier
Un
signal()est une valeur réactive : on la lit en l’appelant, et on la modifie avecset()ouupdate(). Uncomputed()est une valeur en lecture seule dérivée d’autres signaux ; il est lazy et mémoïsé, recalculé uniquement quand ses dépendances changent.Puis allez plus loin
- Les dépendances sont suivies automatiquement : tous les signaux lus par la fonction deviennent des dépendances, sans abonnement à gérer.
- Les signaux se comparent avec
===:items().push(x)ne notifie personne ; utilisezupdate((list) => [...list, x]). - Règle empirique : tout ce qui peut être calculé à partir d’un autre état est un
computed(), pas un second signal à garder synchronisé. Exposez l’état avecasReadonly().
export class Counter { protected readonly count = signal(0); protected readonly double = computed(() => this.count() * 2); protected increment() { this.count.update((c) => c + 1); } } // template: {{ count() }} · {{ double() }}On vous demandera probablement ensuite
Pourquoi l’interface ne se met-elle pas à jour après
items().push(x)?Signal d’alerte
Tente d’appeler
set()sur uncomputed(), ou oublie les parenthèses et lie la fonction du signal au lieu de sa valeur.J-11Qu’est-ce que
effect(), et quand ne faut-il pas l’utiliser ? SignalsÀ dire en premier
effect()exécute une fonction une première fois, puis à nouveau chaque fois que les signaux qu’elle lit changent. Il sert aux effets de bord vers le monde extérieur —localStorage, journalisation, bibliothèque tierce — pas à calculer de l’état.Puis allez plus loin
- N’utilisez pas un effect pour copier un signal dans un autre : c’est un
computed()(ou unlinkedSignal()s’il doit aussi être modifiable). - Ne récupérez pas de données depuis un effect : il n’y a pas d’annulation et d’anciennes réponses peuvent gagner la course. Utilisez
httpResource()/resource(). - Il doit être créé dans un contexte d’injection (constructeur ou champ), il est détruit avec le composant, et
onCleanupnettoie les timers avant l’exécution suivante.
On vous demandera probablement ensuite
Que se passe-t-il si vous créez un effect dans
ngOnInit?Signal d’alerte
Utilise
effect()pour garderfullNamesynchronisé avecfirstNameetlastName.- N’utilisez pas un effect pour copier un signal dans un autre : c’est un
J-12Quelle est la différence entre le constructeur et
ngOnInit? Comment nettoyer quand un composant est détruit ? ComposantsÀ dire en premier
Le constructeur s’exécute à la création de la classe et constitue le contexte d’injection :
inject(), les signaux et les effects y vont, ou dans les initialiseurs de champs.ngOnInits’exécute une fois, après la définition des premiers inputs. Pour le nettoyage, j’utiliseinject(DestroyRef).onDestroy()outakeUntilDestroyed().Puis allez plus loin
- Lire un
input.required()dans le constructeur lève une erreur, car le parent ne l’a pas encore lié. Avec les signal inputs,ngOnInitest rarement nécessaire : uncomputed()ou unhttpResource()lit l’input quand il est prêt. inject()ne fonctionne pas dansngOnInitni dans un gestionnaire de clic — c’est hors du contexte d’injection (erreur NG0203).DestroyReffonctionne aussi dans les services, les directives et les fonctions réutilisables, ce quengOnDestroyne peut pas faire. Pour accéder au DOM après le rendu, utilisezafterNextRender().
export class Clock { readonly now = signal(new Date()); constructor() { const id = setInterval(() => this.now.set(new Date()), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } }On vous demandera probablement ensuite
Où initialiseriez-vous une bibliothèque de graphiques qui a besoin de l’élément DOM ?
Signal d’alerte
Dit que le constructeur sert « uniquement à la DI » et met toute la logique dans
ngOnInit, ou ne nettoie jamais les intervalles et les abonnements.- Lire un
J-13Qu’est-ce qu’un service et comment l’utilise-t-on dans un composant ? DI
À dire en premier
Un service est une classe qui porte de la logique ou un état partagés entre composants — appels d’API, panier, utilisateur connecté. En v22, je le marque avec
@Service(), qui est fourni en root comme singleton, et je l’obtiens avecinject(CartService).Puis allez plus loin
@Service()équivaut à@Injectable({ providedIn: 'root' }): créé à la première injection et compatible avec le tree-shaking. Il imposeinject()— la DI par constructeur est une erreur de compilation.@Injectablereste valide (code existant, bibliothèques pour des versions plus anciennes). Placer le service dans lesprovidersd’un composant donne une instance par composant au lieu d’une par application.inject()fonctionne aussi dans les guards fonctionnels, les resolvers et les interceptors, et évite les longs appelssuper(...)avec l’héritage.
@Service() export class CartService { private readonly _items = signal<CartItem[]>([]); readonly items = this._items.asReadonly(); readonly count = computed(() => this._items().length); add(item: CartItem) { this._items.update((list) => [...list, item]); } }On vous demandera probablement ensuite
Comment donneriez-vous à chaque instance d’un composant d’assistant (wizard) sa propre copie d’un service ?
Signal d’alerte
Crée le service avec
new CartService(), ou pense qu’un service dansprovidersest toujours le même singleton global.J-14Comment configurer le routing et naviguer entre les pages ? Routing
À dire en premier
On définit un tableau
Routes, on l’enregistre avecprovideRouter(routes), et on place<router-outlet />là où les pages s’affichent. On navigue avecrouterLinkdans les templates etrouter.navigate()dans le code ; les pages sont généralement chargées en lazy loading avecloadComponent.Puis allez plus loin
loadComponent: () => import('./home-page')place la page dans un chunk séparé, téléchargé seulement quand la route est visitée ;loadChildrenfait de même pour toute une fonctionnalité.- L’ordre compte — la première correspondance l’emporte — donc le wildcard
'**'va en dernier.routerLinkActiveajoute une classe au lien actif. - Utilisez
routerLink, pashref:hrefrecharge toute l’application.
On vous demandera probablement ensuite
Comment protégeriez-vous une route pour que seuls les utilisateurs connectés puissent l’ouvrir ?
Signal d’alerte
Navigue avec
<a href>ouwindow.location, ou place la route wildcard en premier.J-15Comment une page lit-elle un paramètre de route comme
:id? RoutingÀ dire en premier
Avec
withComponentInputBinding()dansprovideRouter, les paramètres de chemin, les query params et les données de route arrivent comme inputs du composant :readonly id = input.required({ transform: numberAttribute }). Comme c’est un signal, tout ce qui en dérive se met à jour quand l’id change.Puis allez plus loin
- Piège : naviguer de
/products/1à/products/2réutilise la même instance de composant ; le code qui lit l’id une seule fois (p. ex. un snapshot dansngOnInit) affiche des données périmées. - Les paramètres sont des chaînes dans l’URL, donc un transform comme
numberAttributevous donne un vrai nombre. - Depuis la v22, les routes enfants héritent par défaut des paramètres et des données de tous leurs parents (
paramsInheritanceStrategy: 'always').
@Component({ templateUrl: './product-page.html' }) export default class ProductPage { readonly id = input.required({ transform: numberAttribute }); // :id readonly tab = input<'specs' | 'reviews'>('specs'); // ?tab= readonly product = httpResource<Product>(() => '/api/products/' + this.id()); }On vous demandera probablement ensuite
Qu’est-ce qui casse si vous ne lisez l’id qu’une fois avec
route.snapshot?Signal d’alerte
Ne connaît que
route.snapshot.paramMap.get('id')et ignore que le composant peut être réutilisé.- Piège : naviguer de
J-16Comment appeler une API REST dans Angular ? HTTP
À dire en premier
Avec
HttpClient, injecté dans un service :http.get<Product[]>('/api/products')renvoie un Observable. Depuis la v21, il est disponible en root sans configuration ;provideHttpClient()ne sert qu’à configurer, par exemple des interceptors.Puis allez plus loin
- L’Observable est cold : aucune requête n’est envoyée tant que personne ne s’abonne (
subscribe(),firstValueFrom(), la pipe async). - Pour des lectures réactives dans un composant,
httpResource()fournitvalue(),isLoading()eterror()sous forme de signaux et annule les anciennes requêtes ; pour les écritures (POST, PUT), on appelleHttpClient. - Le générique
get<T>n’est qu’une assertion de type, pas une validation. En v22, Fetch est le backend par défaut ; les préoccupations transverses (jeton d’authentification, erreurs) vont dans des interceptors fonctionnels.
On vous demandera probablement ensuite
Où ajouteriez-vous un en-tête
Authorizationà chaque requête ?Signal d’alerte
Appelle
http.get()sans s’abonner et se demande pourquoi rien ne se passe, ou appellefetchdirectement depuis les composants.- L’Observable est cold : aucune requête n’est envoyée tant que personne ne s’abonne (
J-17Comment construire un formulaire avec validation dans Angular 22 ? Formulaires
À dire en premier
Pour les nouveaux formulaires, j’utilise les Signal Forms, stables en v22 : un signal porte le modèle,
form(model, schema)construit un arbre de champs avec des règles commerequired(p.email), et[formField]lie chaque input. Les Reactive Forms restent le bon choix pour les applications existantes qui en sont pleines, ou les bibliothèques qui ne supportent queControlValueAccessor.Puis allez plus loin
- Le signal du modèle est la source de vérité — pas de copie interne, pas de
patchValue:model.set(data)remplit le formulaire, et le formulaire écrit directement dans le modèle. - L’état de chaque champ est fait de signaux :
f.email().invalid(),touched(),errors(); le formulaire entier est aussi un champ (f().valid(),f().submitting()). - Affichez les erreurs seulement après
touched(), et validez toujours aussi côté serveur — la validation côté client relève de l’UX, pas de la sécurité.
export class LoginForm { readonly model = signal({ email: '', password: '' }); readonly loginForm = form(this.model, (p) => { required(p.email); email(p.email); minLength(p.password, 8); }); } // template: <input [formField]="loginForm.email" />On vous demandera probablement ensuite
Comment valideriez-vous que deux champs de mot de passe correspondent ?
Signal d’alerte
Valide à la main dans le gestionnaire de soumission avec des
if, ou dit que les Signal Forms sont encore seulement expérimentaux en v22.- Le signal du modèle est la source de vérité — pas de copie interne, pas de
J-18Que fait
@defer? PerformanceÀ dire en premier
@defercharge en lazy une partie d’un template : les composants qu’il contient vont dans un chunk séparé, téléchargé seulement quand un déclencheur se produit —on idlepar défaut, ouon viewport,on interaction,when cond(). Il réduit le bundle initial sans toucher aux routes.Puis allez plus loin
@placeholders’affiche avant le déclencheur (requis pouron viewport/interaction/hover),@loadingpendant le téléchargement,@errorsi le téléchargement échoue.- Piège : si un composant différé est aussi référencé ailleurs dans le même fichier (hors du bloc), il est chargé immédiatement et le chunk est perdu.
prefetch on hovertélécharge tôt et affiche plus tard — idéal pour les actions « à un clic ».
@defer (on viewport) { <app-comments [postId]="post().id" /> } @placeholder { <div class="comments-skeleton"></div> } @loading (after 100ms) { <app-spinner /> }On vous demandera probablement ensuite
Quelle est la différence entre
@deferet@if?Signal d’alerte
Pense que
@defern’est qu’un@ifretardé et ne mentionne pas le chunk JavaScript séparé.J-19Comment Angular sait-il quand mettre à jour l’écran ? Qu’a changé la v21, et la v22 ? Change Detection
À dire en premier
La Change Detection réaffiche les templates quand quelque chose notifie Angular : un signal lu dans le template change, un événement de template se déclenche, la pipe async émet, ou
markForCheck()est appelé. Depuis la v21, les nouvelles applications sont zoneless (sans zone.js), et depuis la v22 les composants sontOnPushpar défaut.Puis allez plus loin
- Sans zone.js, un
setTimeoutou unsubscribe()qui modifie un simple champ ne met pas l’interface à jour. La solution est un signal (outoSignal()/ la pipe async). - Un composant
OnPushn’est vérifié que lorsqu’un input reçoit une nouvelle référence, qu’un événement s’y produit, qu’un signal qu’il lit change, ou qu’il est marqué pour vérification — sinon son sous-arbre est ignoré. ChangeDetectionStrategy.Eagerest l’ancien comportement « toujours vérifier » (Defaultest déprécié) ;ng updatel’ajoute là où c’est nécessaire.
On vous demandera probablement ensuite
Une valeur définie dans un
setTimeoutn’apparaît pas à l’écran. Pourquoi, et comment corriger ?Signal d’alerte
Dit que « zone.js détecte automatiquement chaque changement » pour une application v21+, ou corrige les mises à jour manquantes en semant des
detectChanges()partout.- Sans zone.js, un
J-20Comment tester un composant dans un nouveau projet Angular ? Tests
À dire en premier
Depuis la v21,
ng testexécute Vitest (Karma est déprécié). Je crée le composant avecTestBed.createComponent(), je définis les inputs, j’attendsawait fixture.whenStable()pour que le rendu zoneless se termine, puis je vérifie surfixture.nativeElement.Puis allez plus loin
- Testez le comportement que voit l’utilisateur (texte, clics, outputs émis), pas les détails d’implémentation privés.
- Les inputs peuvent être passés comme signaux avec
inputBinding(); les outputs sont espionnés avecoutputBinding()etvi.fn(). - Pour le HTTP, utilisez
provideHttpClientTesting()etHttpTestingControllerplutôt qu’un vrai serveur ; les services et les signal stores se testent avecTestBed.inject()et sans DOM.
it('shows the right number of stars', async () => { const fixture = TestBed.createComponent(StarRating, { bindings: [inputBinding('value', () => 3)], }); await fixture.whenStable(); const stars = fixture.nativeElement.querySelectorAll('button'); expect(stars).toHaveLength(5); });On vous demandera probablement ensuite
Comment testeriez-vous un composant qui charge des données via HTTP ?
Signal d’alerte
N’a jamais écrit de test, ou s’appuie sur
fakeAsync/tickde zone.js dans un projet Vitest zoneless.
Confirmé : construire des fonctionnalités en autonomie
À ce niveau, les questions portent sur des choix : computed ou effect, quel opérateur RxJS, où vit un provider, comment un composant est découpé, comment vous le testez. Une bonne réponse de niveau confirmé nomme l’option, la raison, et le cas où l’autre option l’emporte.
M-01Quand utilise-t-on
computed()et quandeffect()? Pourquoi ne faut-il pas définir de signaux dans un effect ? SignalsÀ dire en premier
computed()sert à l’état dérivé : une valeur pure, lazy et mémoïsée.effect()sert aux effets de bord qui poussent des valeurs de signaux vers le monde non réactif (DOM,localStorage, journalisation). Passer de signal à signal via un effect, c’est presque toujours uncomputed()ou unlinkedSignal()déguisé.Puis allez plus loin
- Définir un signal dans un effect, c’est une exécution supplémentaire, un moment où la cible est périmée (un rendu avec une valeur erronée) et un graphe de dépendances difficile à suivre.
- Les effects ne sont pas synchrones : définissez un signal trois fois et l’effect s’exécute une seule fois, avec la valeur finale. Les effects de composant s’exécutent pendant la Change Detection ; ceux des services root s’exécutent comme microtâches.
- Les dépendances sont dynamiques : seuls les signaux lus lors de la dernière exécution sont suivis. Utilisez
untracked()pour les lectures qui ne doivent pas redéclencher l’effect. - Un effect a besoin d’un contexte d’injection (constructeur ou initialiseur de champ) ou d’un
injectorexplicite ; sinon vous obtenez NG0203. UtilisezonCleanuppour les timers et les listeners. - Du HTTP depuis un effect est un mauvais signe : pas d’annulation, donc les réponses peuvent arriver dans le désordre. C’est précisément le rôle de
resource/httpResource.
readonly firstName = signal('Ana'); readonly lastName = signal('Pop'); // ✗ effect used to synchronise state // effect(() => this.fullName.set(this.firstName() + ' ' + this.lastName())); // ✓ derived state readonly fullName = computed(() => this.firstName() + ' ' + this.lastName()); // ✓ effect: reactive world → outside world constructor() { effect(() => localStorage.setItem('name', this.fullName())); }On vous demandera probablement ensuite
Et si la valeur dérivée doit aussi pouvoir être modifiée par l’utilisateur ?
Signal d’alerte
« J’utilise effect pour garder deux signaux synchronisés » ou « computed et effect, c’est pareil, effect s’exécute juste plus souvent ».
M-02Quel problème
linkedSignal()résout-il ? Donnez un exemple concret. SignalsÀ dire en premier
C’est un signal modifiable qui se réinitialise à partir d’une source : dérivé comme
computed(), mais on peut aussi leset(). Cas typiques : une sélection qui prend par défaut la première option d’une liste, un numéro de page qui revient à 1 quand les filtres changent, une copie modifiable d’un input.Puis allez plus loin
- Il remplace le schéma fragile signal + effect : pas d’exécution supplémentaire, pas de rendu avec une valeur périmée.
- La forme longue (
source+computation) vous donne la valeurprevious, ce qui permet de conserver le choix de l’utilisateur s’il existe encore dans la nouvelle liste. - Un
set()manuel n’est valable que jusqu’au prochain changement de la source ; ensuite, c’est la computation qui l’emporte. - Depuis la v22.1, l’option
setintercepte les écritures, p. ex. pour plafonner une quantité au stock disponible.
On vous demandera probablement ensuite
Comment construiriez-vous un brouillon modifiable d’un
input()avec un indicateur « dirty » ?Signal d’alerte
Copier l’input dans un signal dans
ngOnInit(il ne se réinitialise jamais) ou synchroniser avec un effect.M-03Comment fonctionnent
resource/httpResource, et pourquoi sont-ils préférables à un abonnement HTTP dans un effect ? HTTPÀ dire en premier
Un resource transforme le chargement asynchrone en signaux : une fonction
params(ou URL) réactive, et quand elle change, l’ancienne requête est annulée et une nouvelle démarre. Vous obtenez gratuitementvalue(),status(),isLoading(),error()etreload(), sans race condition.Puis allez plus loin
resourcereçoit unabortSignaldans le loader ;httpResourcepasse parHttpClient, donc les interceptors s’appliquent ;rxResourceprend un flux Observable et se comporte commeswitchMap.- Renvoyer
undefineddepuis params donne le statutidle: le loader ne s’exécute pas. En v22, vous pouvez aussi leverResourceParamsStatus.IDLE/LOADING, et chaîner des resources avecctx.chain(). hasValue()est un type guard ;defaultValuesupprimeundefined;parsevalide ou transforme la réponse (p. ex. DTO → modèle, Zod).- Les resources servent à la lecture. Les écritures (POST/PUT/DELETE) passent par
HttpClient, puis vous faites unupdate()local du resource (optimiste) ou unreload().
readonly userId = input.required<number>(); // a new id → the old request is cancelled, a new one starts readonly user = httpResource<User>(() => '/api/users/' + this.userId()); // template: // @if (user.isLoading()) { <app-spinner /> } // @if (user.error()) { <button (click)="user.reload()">Retry</button> } // @else if (user.hasValue()) { <h2>{{ user.value().name }}</h2> }On vous demandera probablement ensuite
Comment supprimeriez-vous un élément d’une liste chargée avec
httpResourceet géreriez-vous un échec ?Signal d’alerte
Utiliser
httpResourcepour un POST au clic sur un bouton, ou ne pas savoir ce qu’il advient d’une requête en cours quand les params changent.M-04Comment implémenter une recherche à la frappe sans lancer une requête à chaque touche ? Signals
À dire en premier
En v22, j’enveloppe le signal de la requête avec
debounced(query, 300)et j’injecte savalue()dans unhttpResource. Le debounce supprime le bruit ; le resource annule les requêtes périmées, donc la dernière requête gagne toujours.Puis allez plus loin
debounced()renvoie unResource:value()se met à jour après le dernier changement, etisLoading()vaut true tant que l’utilisateur tape encore.- Il est expérimental en v22. L’alternative classique est
toSignal(toObservable(query).pipe(debounceTime(300), distinctUntilChanged())): trois conversions pour un seul debounce. waitpeut être une fonction, p. ex. pas de délai quand le champ est vidé.- Dans les Signal Forms, il existe une règle dédiée
debounce(path.field, 300), etvalidateHttpa sa propre optiondebounce.
import { httpResource } from '@angular/common/http'; import { debounced, signal } from '@angular/core'; export class UserSearch { readonly query = signal(''); readonly search = debounced(this.query, 300); readonly users = httpResource<UserDto[]>( () => '/api/users?q=' + encodeURIComponent(this.search.value()), { defaultValue: [] }, ); }On vous demandera probablement ensuite
Le debounce seul n’empêche pas les réponses dans le désordre. Qu’est-ce qui le fait ?
Signal d’alerte
Un
setTimeoutdans le gestionnaire d’input avec unsubscribe()manuel et sans annulation de la requête précédente.M-05Comment combiner signaux et RxJS dans un composant sans fuite d’abonnements ? Quand garder RxJS ? RxJS
À dire en premier
Des signaux pour l’état affiché, RxJS aux frontières (événements, flux, asynchrone complexe).
toSignal()s’abonne et se désabonne automatiquement ; pour unsubscribe()manuel, j’ajoutetakeUntilDestroyed(), qui le lie auDestroyRefdu composant.Puis allez plus loin
toSignal()sansinitialValueest typéT | undefined;requireSync: truene fonctionne que si la source émet de façon synchrone (p. ex. unBehaviorSubject).toObservable()émet de façon asynchrone (via un effect) et uniquement la dernière valeur si le signal est modifié plusieurs fois dans le même tick — ce n’est pas un flux d’événements.takeUntilDestroyed()sans argument ne fonctionne que dans un contexte d’injection ; ailleurs, passez explicitementinject(DestroyRef). Pour un nettoyage hors RxJS, utilisezDestroyRef.onDestroy().- Gardez RxJS pour les websockets, la combinaison de flux d’événements, le retry/backoff et les politiques d’aplatissement (
switchMap,exhaustMap…). Les signaux n’ont aucune notion du temps.
On vous demandera probablement ensuite
Pourquoi ne peut-on pas appeler
toSignal()dans un gestionnaire de clic ?Signal d’alerte
« Les signaux remplacent complètement RxJS » — ou l’inverse : s’abonner partout et copier les valeurs dans des champs sans jamais se désabonner.
M-06Expliquez
switchMapvsmergeMapvsconcatMapvsexhaustMap, avec un cas d’usage pour chacun. RxJSÀ dire en premier
Ils diffèrent par ce qui se passe quand une nouvelle valeur arrive alors que l’Observable interne précédent tourne encore :
switchMapl’annule (recherche),mergeMapexécute en parallèle (uploads),concatMapmet en file dans l’ordre (sauvegardes ordonnées),exhaustMapignore la nouvelle valeur (soumission, connexion).Puis allez plus loin
- Choisir l’opérateur, c’est choisir une politique de concurrence ; un mauvais choix cause de vrais bugs : des commandes en double avec
mergeMapsur un bouton de paiement, des sauvegardes perdues avecswitchMapsur une sauvegarde automatique. mergeMapaccepte une limite de concurrence, p. ex.mergeMap(upload, 3).- Mettez
catchErrordans le pipe interne : après l’opérateur d’aplatissement, la première erreur termine le flux externe et la recherche « meurt ». - Avec les signaux,
httpResourceetrxResourcevous donnent déjà la sémantique deswitchMappour les lectures.
// the last one wins; errors handled INSIDE, so the stream survives query$.pipe( debounceTime(300), switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))), ); files$.pipe(mergeMap((file) => this.api.upload(file), 3)); // parallel, max 3 patches$.pipe(concatMap((patch) => this.api.save(patch))); // in order submit$.pipe(exhaustMap(() => this.api.login(this.credentials()))); // ignore double clicksOn vous demandera probablement ensuite
Lequel utiliseriez-vous pour la sauvegarde automatique d’un document, et pourquoi pas
switchMap?Signal d’alerte
« J’utilise toujours
switchMap» ou des appelssubscribe()imbriqués au lieu d’un opérateur d’aplatissement.- Choisir l’opérateur, c’est choisir une politique de concurrence ; un mauvais choix cause de vrais bugs : des commandes en double avec
M-07Dans une application Angular 22 zoneless, qu’est-ce qui déclenche la Change Detection, et pourquoi un champ modifié dans
setTimeoutpeut-il ne pas s’afficher ? Change DetectionÀ dire en premier
Sans zone.js, rien ne patche les timers ni les promises. La Change Detection est planifiée par le changement d’un signal lu dans le template, un événement de template/host, la pipe
async,markForCheck(),setInput()ou une navigation. Un simple champ modifié danssetTimeoutousubscribe()ne notifie personne — faites-en un signal.Puis allez plus loin
- Depuis la v22, les composants sans
changeDetectionsont OnPush ; l’ancien comportement estChangeDetectionStrategy.Eager(Defaultest déprécié).ng updateajoute Eager là où c’est nécessaire. - Un composant OnPush n’est vérifié que pour une nouvelle référence d’input (
===), un événement en lui ou dans ses enfants, un signal lu dans son template, la pipeasync, oumarkForCheck(). Sinon, tout le sous-arbre est ignoré. - Muter un tableau passé en input (
push) ne change pas la référence : faites des mises à jour immuables. markForCheck()est la rustine d’urgence pour les callbacks hérités ;detectChanges()est synchrone et rarement nécessaire.
// ✗ works with zone.js, NOT zoneless + OnPush // now = new Date(); // setInterval(() => (this.now = new Date()), 1000); // ✓ a signal schedules change detection by itself export class Clock { readonly now = signal(new Date()); constructor() { const id = setInterval(() => this.now.set(new Date()), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } }On vous demandera probablement ensuite
Comment migreriez-vous, étape par étape, un ancien composant reposant sur Zone.js vers OnPush + zoneless ?
Signal d’alerte
Appeler
detectChanges()partout, ou croire qu’OnPush signifie « le composant ne se met jamais à jour sauf si les inputs changent ».- Depuis la v22, les composants sans
M-08En quoi
viewChild()etcontentChildren()diffèrent-ils des anciens décorateurs, et quand peut-on les lire en toute sécurité ? ComposantsÀ dire en premier
Les signal queries renvoient des signaux, donc les résultats sont réactifs : vous en dérivez avec
computed()au lieu dengAfterViewInitet deQueryList.changes. La vue n’existe pas dans le constructeur, donc le travail sur le DOM va dansafterNextRender();viewChild.required()lève une erreur s’il est lu trop tôt.Puis allez plus loin
viewChildcherche dans le template propre du composant ;contentChildcherche dans ce que le parent a projeté (p. ex. un conteneur Tabs qui découvre ses enfants Tab).contentChildrenne regarde que les enfants directs par défaut ; utilisezdescendants: truepour des correspondances plus profondes.readchoisit ce que vous obtenez :ElementRef,TemplateRef,ViewContainerRef, une directive ou (v22.2) l’Injectorde l’enfant.- Piège : interroger un composant par son type dans la classe le rend eager, donc il ne sera pas séparé par un bloc
@defer.
On vous demandera probablement ensuite
Comment un composant Tabs n’afficherait-il que le contenu de l’onglet actif ?
Signal d’alerte
Lire une view query dans le constructeur ou
ngOnInitet s’étonner qu’elle soitundefined.M-09Projection de contenu avec
ng-contentvs passage d’unng-templaterendu avecngTemplateOutlet: quand choisir l’un ou l’autre ? TemplatesÀ dire en premier
ng-contentsert aux emplacements statiques : le parent crée le contenu une fois, que l’enfant l’affiche ou non. Unng-templateest une recette que l’enfant instancie quand et aussi souvent qu’il le veut, avec un contexte — nécessaire pour le contenu lazy et pour les API du type « à quoi doit ressembler une ligne ».Puis allez plus loin
- Envelopper
ng-contentdans@ifmasque le DOM, mais les composants projetés existent déjà et leur lifecycle a déjà tourné. - Les emplacements nommés utilisent
select(balise, attribut, classe) ;ngProjectAsprojette plusieurs éléments dans un même emplacement sans wrapper. - Le contexte renvoie des données :
$implicitcorrespond àlet-item. Sans directive avecngTemplateContextGuard, les variableslet-sont de typeany. - Un template voit l’injector de l’endroit où il est déclaré ; depuis la v21.2,
[ngTemplateOutletInjector]="'outlet'"lui fait utiliser l’injector de l’endroit où il est rendu.
@Component({ selector: 'app-collapsible', imports: [NgTemplateOutlet], template: ` <button (click)="open.set(!open())">Toggle</button> @if (open()) { <ng-container [ngTemplateOutlet]="content() ?? null" /> } `, }) export class Collapsible { readonly content = contentChild(TemplateRef); // created only when opened readonly open = signal(false); } // <app-collapsible><ng-template><app-heavy-report /></ng-template></app-collapsible>On vous demandera probablement ensuite
Comment typer fortement les variables
let-d’un template ?Signal d’alerte
Penser que
@ifautour deng-contentrend le contenu projeté lazy.- Envelopper
M-10Comment construiriez-vous un composant de tableau de données réutilisable et typé, sans en faire un « composant fourre-tout » ? Composants
À dire en premier
Rendez le composant générique (
DataTable<T>) :Test inféré depuisrows, les clés de colonnes sont restreintes àkeyof T, les outputs sont typés. La personnalisation passe par des templates de cellule, pas par des inputs booléens, et les wrappers propres au domaine vivent dans la feature.Puis allez plus loin
- Le signe du composant fourre-tout : des données
any[], des flagsisProductTable, 30 inputs et de la connaissance métier dansshared/. - Les templates de cellule sont trouvés avec
contentChildren()sur une directive ;ngTemplateContextGuardtypelet-rowenTau lieu deany. - Passez le comportement sous forme de fonctions (
trackBy,displayWith,compareWith) pour que le composant n’ait pas à connaître la forme des données. - Une faute de frappe dans une clé de colonne devient une erreur de compilation, pas une colonne vide en production.
On vous demandera probablement ensuite
Où vivrait le tableau spécifique aux utilisateurs, et qu’ajouterait-il au tableau générique ?
Signal d’alerte
Ajouter un nouvel input booléen chaque fois qu’une page a besoin de quelque chose de différent.
- Le signe du composant fourre-tout : des données
M-11Comment afficher un composant choisi à l’exécution, p. ex. des widgets de tableau de bord issus d’une config ? Composants
À dire en premier
Montez une échelle : un petit ensemble connu →
@switch; le composant est une valeur →NgComponentOutletavecinputs; contrôle total (outputs, directives) →ViewContainerRef.createComponent()avecbindings(inputBinding,outputBinding,twoWayBinding).Puis allez plus loin
- Un registre de loaders
() => import(…)fait de chaque widget un chunk lazy distinct. inputBinding('title', () => …)est réactif, contrairement à l’anciensetInput()qu’il fallait rappeler à chaque changement, avec en plus des abonnements manuels aux outputs.ComponentFactoryResolvera été supprimé en v22 : on passe directement la classe àcreateComponent.- Pour les toasts et boîtes de dialogue hors de l’arbre, utilisez le
createComponent()standalone avec unEnvironmentInjector, puisappRef.attachView()pour qu’il rejoigne la Change Detection, et détruisez-le vous-même.
private readonly vcr = inject(ViewContainerRef); private async render(type: WidgetType) { const component = await WIDGET_REGISTRY[type](); // lazy chunk this.vcr.clear(); this.vcr.createComponent(component, { bindings: [ inputBinding('title', () => this.config().title), // stays in sync outputBinding('remove', () => this.removed.emit(this.config().id)), ], }); }On vous demandera probablement ensuite
Comment s’assurer que tous les widgets acceptent les mêmes inputs et outputs ?
Signal d’alerte
Recourir à
ComponentFactoryResolverou àinnerHTMLavec des balises de composants.- Un registre de loaders
M-12Comment gérer l’état d’une page de fonctionnalité sans bibliothèque d’état ? Où se placent les composants smart et dumb ? Architecture
À dire en premier
Un service store basé sur les signaux : des signaux modifiables privés, des signaux publics en lecture seule (
asReadonly()) et des sélecteurscomputed(), plus des méthodes aux noms métier qui font des mises à jour immuables. La page routée (smart) fournit et injecte le store ; les composants dumb reçoivent les données par des inputs et émettent des intentions par des outputs.Puis allez plus loin
- Délimitez par durée de vie : l’état d’une fonctionnalité dans les
providersde la page ou de la route (il meurt avec elle), l’état global (utilisateur, panier, thème) dans un@Service()root. - Les données serveur vont dans un
httpResourceà l’intérieur du store ; l’état qui doit survivre à un rafraîchissement ou être partageable va dans l’URL (query params +input()). - Un composant dumb peut avoir un état d’UI local et injecter des éléments d’UI (
ElementRef,DestroyRef), mais jamais de services de données ni le routeur — sinon il n’est ni réutilisable ni facile à tester, et une liste de 50 cartes fait 50 requêtes. - NgRx SignalStore devient intéressant quand l’équipe veut des conventions, des plugins et des devtools, pas par défaut.
On vous demandera probablement ensuite
Pourquoi exposer des signaux en lecture seule plutôt que les signaux modifiables ?
Signal d’alerte
Mettre chaque morceau d’état dans un unique service root global, ou exposer des signaux modifiables que n’importe quel composant mute sur place.
- Délimitez par durée de vie : l’état d’une fonctionnalité dans les
M-13Expliquez la hiérarchie de la DI : quand fournir en root, sur une route ou sur un composant, et quand faut-il un
InjectionToken? DIÀ dire en premier
inject()cherche dans les injectors d’éléments (l’élément courant, puis ses parents DOM), puis l’injector de route, root et platform, et lève NG0201 si rien ne correspond. Root (@Service()) signifie un singleton pour toute l’application ; lesprovidersd’une route donnent une instance par fonctionnalité ; lesprovidersd’un composant, une instance par composant et ses enfants. Les tokens servent aux valeurs qui ne sont pas des classes : config, URLs, fonctions.Puis allez plus loin
@Service()(v22) équivaut àprovidedIn: 'root', est compatible avec le tree-shaking et n’autorise queinject().autoProvided: falsevous oblige à le lister dansproviders.inject()l’emporte sur la DI par constructeur : pas de chaînessuper(…), il fonctionne dans les guards, les interceptors et les factories, et permet des fonctionsinjectX()réutilisables. Il exige un contexte d’injection (sinon NG0203).useClass,useValue,useFactory,useExistingetmulti: truepour les tableaux de type plugin ;optional,self,skipSelf,hostmodifient la recherche.- Les
viewProviderssont invisibles pour le contenu projeté ; lesEnvironmentProviders(provideX()) ne peuvent aller qu’au niveau de l’application ou de la route, pas dans un composant.
On vous demandera probablement ensuite
Que devient un service fourni sur une route lazy quand l’utilisateur navigue ailleurs ?
Signal d’alerte
Lister un service root dans les
providersd’un composant « par sécurité » — ce qui crée une seconde instance et des bugs d’état difficiles à trouver.M-14Guards et resolvers fonctionnels : quelle différence entre
canMatchetcanActivate, et quand éviteriez-vous un resolver ? RoutingÀ dire en premier
Les guards sont de simples fonctions qui utilisent
inject().canMatchdécide si la route « existe » pour l’utilisateur — sinon le routeur essaie la route suivante et le chunk lazy n’est pas téléchargé.canActivatebloque l’entrée dans une route qui a correspondu. Un resolver retarde la navigation jusqu’à l’arrivée des données ; pour les listes et les tableaux de bord, je préfèrehttpResourcedans la page.Puis allez plus loin
- Renvoyez
true/false, unUrlTreeou unRedirectCommand(redirection + options de navigation commereplaceUrl) ; depuis la v22.2, vous pouvez aussi lever unRedirectCommand. - Resolver : la page apparaît avec ses données prêtes, mais la navigation attend et une erreur l’annule.
httpResource: navigation instantanée, chargement et erreur sont de l’état d’UI. - Avec
withComponentInputBinding(), les données résolues, les params et les query params arrivent sous forme d’input(). - Les guards relèvent de l’UX, pas de la sécurité : l’API doit aussi faire respecter l’autorisation.
canDeactivateprotège les modifications non enregistrées.
On vous demandera probablement ensuite
Comment afficheriez-vous un tableau de bord différent pour les admins et les utilisateurs ordinaires sur le même chemin ?
Signal d’alerte
Des guards à base de classes avec injection par constructeur comme choix par défaut, ou traiter un guard comme seul contrôle d’accès.
- Renvoyez
M-15Écrivez un interceptor HTTP fonctionnel qui ajoute un jeton d’authentification. Quels sont les pièges ? HTTP
À dire en premier
Un
HttpInterceptorFnest une fonction(req, next)qui renvoienext(req)— ou un clone de la requête, car les requêtes sont immuables. Il s’enregistre avecprovideHttpClient(withInterceptors([…]))et peut utiliserinject().Puis allez plus loin
- N’attachez le jeton qu’à votre propre API (vérifiez l’URL), jamais à des hôtes tiers.
- L’ordre compte : les requêtes traversent les interceptors dans l’ordre indiqué, les réponses reviennent dans l’ordre inverse.
- Appelez
inject()en haut de la fonction, pas dans des callbacks RxJS commecatchErrorqui s’exécutent plus tard, hors du contexte d’injection. - Utilisez
HttpContextTokenpour les métadonnées par requête (p. ex. ignorer la barre de chargement) plutôt que de faux en-têtes.httpResourceetvalidateHttppassent aussi par les interceptors.
export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = inject(AuthStore).token(); if (!token || !req.url.startsWith('/api')) return next(req); return next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } })); }; // app.config.ts provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]));On vous demandera probablement ensuite
Comment géreriez-vous un 401 en rafraîchissant le jeton une seule fois et en rejouant la requête d’origine ?
Signal d’alerte
Muter directement
req.headers, ou envoyer le jeton à toutes les URL.M-16Comment structurer la gestion des erreurs dans une application Angular : erreurs HTTP, erreurs de rendu et tout le reste ? Architecture
À dire en premier
En couches : les erreurs HTTP prévisibles (404, 422) sont de l’état d’UI dans le store ou le composant (
resource.error()) ; les erreurs transverses (401, 5xx) vont dans un interceptor ; une zone défaillante de la page est isolée avec@boundary; tout ce qui n’est pas intercepté finit dans unErrorHandlerglobal personnalisé pour la journalisation et la télémétrie.Puis allez plus loin
@boundary(v22.2, developer preview) possède des blocs@error (let e = $error; let reset = $reset), éventuellement avec des conditionswhen— la première correspondance l’emporte.@boundaryn’intercepte que les erreurs de rendu, pas celles des gestionnaires d’événements ni du code asynchrone — celles-ci atteignent toujours l’ErrorHandler.provideBrowserGlobalErrorListeners()transmetwindow.onerroret les rejets non gérés à l’ErrorHandler.- Un interceptor doit relancer l’erreur (
throwError(() => err)) après sa réaction transverse, afin que l’appelant puisse encore réagir. Les erreurs de validation ne sont pas des exceptions : elles vivent dans leserrors()du formulaire.
@boundary { <app-price-widget [price]="price()" /> <app-recommendations /> } @error (let e = $error; when e instanceof PermissionError) { <p>You don't have access to this widget.</p> } @error (let e = $error; let retry = $reset) { <app-error-card [message]="e.message" (retry)="retry()" /> }On vous demandera probablement ensuite
Où atterrit une erreur levée dans un gestionnaire de clic ?
Signal d’alerte
Un
try/catchavecconsole.logdans chaque composant, ou avaler les erreurs dans un interceptor pour que personne d’autre ne les voie.M-17Comment fonctionne la validation dans les Signal Forms, y compris asynchrone, et en quoi diffère-t-elle des Reactive Forms typés ? Formulaires
À dire en premier
Dans les Signal Forms, le modèle est un simple signal, et
form(model, schema)construit dessus un arbre de champs avec des règles déclarées dans une fonction de schéma (required,email,validate,validateHttp…). Chaque état de champ est un signal :valid(),errors(),pending(),touched(). Les Reactive Forms gardent leur propre copie de la valeur dans desFormControlet exposent les changements sous forme d’Observables.Puis allez plus loin
- Pas de copie interne :
model.set()met à jour les inputs et la saisie met à jour le modèle — nipatchValuenivalueChanges. - Les règles entre champs utilisent
valueOf()dansvalidate; les règles conditionnelles utilisentwhen,hidden,disabled(les champs masqués et désactivés ne sont pas validés) ; les règles réutilisables utilisentschema()+apply(). validateHttppasse parHttpClient(donc les interceptors s’appliquent) et possède une optiondebounceen v22 ;validateAsyncfonctionne avec n’importe quel resource.pending()vaut true pendant son exécution.- Les erreurs serveur issues de
submission.actionpeuvent être renvoyées avec unfieldTreeafin d’atterrir sur le bon champ. - La migration peut être progressive avec
SignalFormControletcompatForm; les Reactive Forms typés (nonNullable,getRawValue()) restent valides pour le code existant.
readonly model = signal({ username: '', password: '' }); readonly signup = form(this.model, (p) => { required(p.username); minLength(p.password, 8, { message: 'At least 8 characters' }); validateHttp(p.username, { request: ({ value }) => '/api/username-available?username=' + value(), debounce: 400, onSuccess: (res: { available: boolean }) => res.available ? undefined : { kind: 'taken', message: 'Username taken' }, onError: () => ({ kind: 'network', message: 'Could not verify' }), }); });On vous demandera probablement ensuite
Comment valideriez-vous que la date de fin est postérieure à la date de début ?
Signal d’alerte
Écrire un validateur asynchrone qui lance une requête à chaque frappe sans debounce, ou ignorer que le champ est
pendingpendant ce temps.- Pas de copie interne :
M-18Comment construire un contrôle de formulaire personnalisé (p. ex. une notation par étoiles) compatible avec les Signal Forms ? Et avec les Reactive Forms ? Formulaires
À dire en premier
Pour les Signal Forms, vous implémentez
FormValueControl<T>: le seul membre obligatoire estvalue = model(); ensuite[formField]fonctionne dessus exactement comme sur uninput. Pour les Reactive Forms, la méthode classique est unControlValueAccessorenregistré avecNG_VALUE_ACCESSOR.Puis allez plus loin
- Les inputs facultatifs comme
disabled,invalid,touchedsont renseignés automatiquement par le formulaire — ne déclarez que ceux que vous utilisez. En v22, un outputtouchsignale que l’utilisateur a touché le contrôle. - Les contrôles booléens implémentent
FormCheckboxControlaveccheckedà la place devalue. - Un CVA nécessite quatre méthodes (
writeValue,registerOnChange,registerOnTouched,setDisabledState) et vous devez appeleronChangevous-même ; oublieronTouchedcasse « afficher les erreurs après le touch ». - Interopérabilité en v22 : les Signal Forms peuvent lier des CVA, et les formulaires template/reactive supportent
FormValueControl, donc un même contrôle peut servir aux deux pendant la migration.
On vous demandera probablement ensuite
Comment le formulaire désactive-t-il votre contrôle, et devez-vous le gérer vous-même ?
Signal d’alerte
Passer un
FormControlen input au composant enfant et le muter là-bas.- Les inputs facultatifs comme
M-19Comment fonctionne
@defer, quels déclencheurs utiliseriez-vous, et qu’est-ce qui rend un bloc différé réellement non lazy ? PerformanceÀ dire en premier
@deferdéplace les composants du bloc dans un chunk séparé chargé sur un déclencheur :on idle(défaut),viewport,interaction,hover,timer,immediateouwhenune condition.prefetchtélécharge plus tôt qu’il n’affiche. Il ne reste lazy que si ces dépendances sont standalone et utilisées nulle part ailleurs dans le fichier.Puis allez plus loin
- Référencer un composant différé dans la classe (p. ex.
viewChild(HeavyChart)) ou ailleurs dans le template le rend eager. @placeholders’affiche avant le déclencheur (et c’est l’élément observé pourviewport/interaction/hoversans cible) ;@loading (after 100ms; minimum 500ms)évite le scintillement ;@errorgère l’échec du téléchargement d’un chunk.whenne se déclenche qu’une fois ; plusieurs déclencheurs se combinent avec un OU.- Nouveau :
on viewport({ rootMargin: '200px' })(v21),on idle(500)(v22). Avec le SSR, les déclencheurshydrate on …donnent l’incremental hydration, activée par défaut en v22.
@defer (on viewport; prefetch on idle) { <app-comments [postId]="post().id" /> } @placeholder (minimum 300ms) { <div class="comments-skeleton"></div> } @loading (after 100ms; minimum 500ms) { <app-spinner /> } @error { <p>We couldn't load the comments.</p> } @defer (on interaction; prefetch on hover) { <app-share-dialog /> } @placeholder { <button>Share</button> }On vous demandera probablement ensuite
Comment vérifier que le bloc s’est vraiment retrouvé dans un chunk séparé ?
Signal d’alerte
Envelopper le contenu above-the-fold dans
@defer, ou penser que@deferretarde une requête HTTP plutôt que le chargement de code.- Référencer un composant différé dans la classe (p. ex.
M-20Comment tester un composant avec des signal inputs et un service qui appelle du HTTP, avec Vitest dans Angular 21+ ? Tests
À dire en premier
Composants :
TestBed.createComponent(Cmp, { bindings: [inputBinding('value', signal)] }), puisawait fixture.whenStable(), puisqu’il n’y a pas de zone à vider. HTTP :provideHttpClient()+provideHttpClientTesting(), puisHttpTestingController.expectOne(url).flush(data)etverify()dansafterEach.Puis allez plus loin
- Vitest est le runner par défaut depuis la v21 (
@angular/build:unit-test) ; Karma est déprécié. Utilisezvi.fn()pour les espions d’outputs etvi.useFakeTimers()à la place defakeAsync. - Modifier le signal lié met à jour l’input comme dans un vrai parent — pas besoin de
setInput(). - Les signal stores sont les plus faciles à tester :
TestBed.inject(), appelez les méthodes, vérifiez lescomputed(). Les effects s’exécutent de façon asynchrone, donc videz-les avecTestBed.tick(). - Testez le comportement via le DOM ou des harnesses, pas via des champs privés.
TestBed.createDirective()(v22.2) teste une directive sans hôte écrit à la main.
beforeEach(() => { TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] }); api = TestBed.inject(ProductsApi); http = TestBed.inject(HttpTestingController); }); afterEach(() => http.verify()); // no unexpected requests it('maps DTO → Product', async () => { const result = firstValueFrom(api.get(1)); http.expectOne('/api/products/1').flush({ id: 1, name: 'Mouse', price: 99, stock: 0 }); expect(await result).toEqual({ id: 1, name: 'Mouse', price: 99, inStock: false }); });On vous demandera probablement ensuite
Comment testeriez-vous un composant qui utilise
httpResource?Signal d’alerte
Interroger le vrai backend dans les tests unitaires, ou appeler
detectChanges()à répétition et vérifier des champs privés.- Vitest est le runner par défaut depuis la v21 (
Senior : rouages internes, architecture, compromis
Les entretiens senior vérifient si vous savez décider pour une équipe : comment le framework fonctionne en dessous, comment une application reste maintenable à 50 développeurs, ce que coûtent le SSR, la sécurité et la performance. Répondez toujours avec une position, le compromis, et la manière dont vous l’introduiriez dans un vrai projet.
S-01Comment les signaux fonctionnent-ils en interne ? Expliquez le graphe producteur/consommateur, le push-pull et pourquoi ils sont sans glitch. Signals
À dire en premier
Les signaux forment un graphe réactif construit à partir des lectures :
signal()est un producteur,computed()est les deux, les effects et les templates sont des consommateurs actifs. Une écriture ne fait qu’incrémenter une version et pousser une notification « peut-être sale » ; les valeurs sont tirées et recalculées paresseusement quand quelqu’un les lit, si bien qu’uncomputedvoit toujours des entrées cohérentes et ne s’exécute qu’une fois par lot d’écritures.Puis allez plus loin
- Les dépendances sont dynamiques : à chaque exécution, la liste des producteurs est reconstruite à partir de ce qui a réellement été lu, donc un signal lu uniquement dans une branche
ifinactive ne déclenche rien. - L’égalité arrête la propagation : la version n’augmente que si la nouvelle valeur n’est pas égale (
Object.ispar défaut). Uncomputeddont le résultat ne change pas ne réveille pas ses consommateurs ;{ equal }personnalise cela pour les valeurs recréées depuis le serveur. - Conséquence : la mutation (
items().push()) ne notifie personne, car la référence et la version restent identiques. L’immuabilité n’est pas une question de style, c’est ainsi que le graphe détecte le changement. - Les effects sont asynchrones et planifiés avec la Change Detection (effects de vue avant la vérification de leur composant, effects root avant tous les composants). Les signaux ne sont pas un mécanisme d’événements et n’ordonnent pas les effets de bord dans le temps — c’est le territoire de RxJS.
const a = signal(1); const b = signal(2); const sum = computed(() => a() + b()); const product = computed(() => a() * b()); const result = computed(() => sum() + product()); // diamond a.set(5); b.set(3); // push: versions bump, nothing is recalculated yet result(); // pull: sum, product, result run once each, with consistent values // equality cutoff: a new array with the same content does not propagate const ids = signal<number[]>([], { equal: (x, y) => x.length === y.length && x.every((v, i) => v === y[i]), });On vous demandera probablement ensuite
Pourquoi un
effect()qui écrit dans un autre signal est-il généralement le mauvais outil, et que faut-il utiliser à la place ?Signal d’alerte
« Un signal, c’est juste un BehaviorSubject avec une meilleure API » ou «
set()réaffiche le composant de façon synchrone » — cela montre qu’on n’a compris ni la paresse, ni les versions, ni le push-pull.- Les dépendances sont dynamiques : à chaque exécution, la liste des producteurs est reconstruite à partir de ce qui a réellement été lu, donc un signal lu uniquement dans une branche
S-02Dans une application zoneless avec des composants OnPush, qu’est-ce qui planifie réellement la Change Detection, et qu’est-ce qu’Angular ignore ? Change Detection
À dire en premier
Sans zone.js, personne ne patche
setTimeoutni les promises, donc Angular n’affiche que lorsqu’on le lui dit : un signal lu dans un template change, un listener de template/host se déclenche, la pipeasyncémet,markForCheck(),setInput()ou une navigation. Un sous-arbre OnPush est ignoré sauf si l’un d’eux le marque, donc le travail est proportionnel à ce qui a changé, pas à la taille de l’arbre.Puis allez plus loin
- Le template est un consommateur réactif : quand un signal qu’il a lu change, Angular marque cette vue pour rafraîchissement (comme
markForCheck) et la Change Detection n’atteint que les composants concernés. - OnPush compare les inputs par référence (
===) : muter un objet passé via un input ne met pas l’enfant à jour — la même règle d’immuabilité que pour les signaux. - Le bug classique : un simple champ modifié dans
setTimeout,subscribe()ou un callback tiers (WebSocket, Maps) — « ça ne se met à jour qu’après un clic quelque part ». La solution est un signal outoSignal();markForCheck()est la porte de secours pour le code hérité. detectChanges()est synchrone et rarement nécessaire (mesures, tests) ;markForCheck()est la variante asynchrone recommandée.
// ✗ worked with zone.js, does nothing zoneless + OnPush export class Clock { now = new Date(); constructor() { setInterval(() => (this.now = new Date()), 1000); } } // ✓ the signal tells Angular what changed export class Clock { readonly now = signal(new Date()); constructor() { const id = setInterval(() => this.now.set(new Date()), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } }On vous demandera probablement ensuite
Un enfant OnPush reçoit un objet par un input et le parent le mute sur place. Que se passe-t-il et comment le corriger au niveau de l’équipe ?
Signal d’alerte
Semer des
detectChanges()partout jusqu’à ce que l’écran se mette à jour, ou croire que zoneless veut dire « Angular vérifie moins souvent, donc il y a plus de bugs ».- Le template est un consommateur réactif : quand un signal qu’il a lu change, Angular marque cette vue pour rafraîchissement (comme
S-03Comment migreriez-vous une grosse application basée sur zone.js vers zoneless sans arrêter le développement de fonctionnalités ? Change Detection
À dire en premier
Progressivement : d’abord rendre les composants corrects sous OnPush avec l’état dans des signaux, pendant que zone.js reste là comme filet de sécurité, puis retirer zone.js une fois que plus rien n’en dépend. Le risque n’est pas Angular, mais le code et les bibliothèques qui comptaient silencieusement sur zone.js pour déclencher les rendus.
Puis allez plus loin
- Commencez par un inventaire : champs simples modifiés dans des callbacks asynchrones,
subscribe()vers des champs, bibliothèques qui attendentNgZone.onStable. Angular 21 fournit un outil d’analyse (onpush_zoneless_migration, via le serveur MCP de la CLI,ng mcp). - Migrez d’abord les composants feuilles, fonctionnalité par fonctionnalité : supprimez
Eager, déplacez l’état vers des signaux /toSignal(), relisez et testez. Chaque PR est petite et réversible. - Jusqu’à la fin, conservez
provideZoneChangeDetection({ eventCoalescing: true })et le polyfill zone.js ; puis retirez les deux et surveillez les bugs de type « ça ne se met à jour qu’après un clic ». - Les tests changent aussi :
fakeAsynca besoin de zone.js, passez donc àawait fixture.whenStable()et aux fake timers de Vitest. - En équipe : une règle de lint/relecture imposant les signaux pour l’état de template dans le nouveau code, et une courte checklist dans le template de PR, pour que la zone migrée ne régresse pas.
On vous demandera probablement ensuite
Une bibliothèque de graphiques tierce met à jour les données dans ses propres callbacks. Comment l’intégrer sans zone.js ?
Signal d’alerte
« Retirer zone.js dans une seule grosse PR et corriger ce qui casse en QA » — pas d’inventaire, pas de filet de sécurité, pas de plan pour les bibliothèques et les tests.
- Commencez par un inventaire : champs simples modifiés dans des callbacks asynchrones,
S-04Comment concevoir la couche d’état d’une grosse application : un service avec des signaux, NgRx SignalStore ou NgRx Store classique ? Architecture
À dire en premier
La première question n’est pas « quelle bibliothèque » mais où vit chaque morceau d’état et qui en est responsable. Un service avec des signaux privés, des sélecteurs
computed()et des méthodes nommées couvre la plupart des applications ; SignalStore quand on veut une forme imposée, des entités et des features réutilisables entre plusieurs équipes ; NgRx Store classique seulement si l’on a vraiment besoin d’un journal d’actions, du time-travel ou de flux pilotés par les événements.Puis allez plus loin
- Gardez l’état au niveau le plus bas qui en a besoin (composant → fonctionnalité → global) et ne le remontez que lorsqu’un nouveau consommateur apparaît. L’état d’une page est fourni sur la route ou le composant, pas en root, pour qu’il ne « colle » pas d’une visite à l’autre.
- Les données serveur sont un cache, pas votre état : gardez-les dans
httpResource/resourceet rechargez après les écritures. L’URL est aussi de l’état (filtres, pagination). - Des règles qui passent à l’échelle : exposer des signaux en lecture seule (
asReadonly()), une seule source de vérité avec le reste dérivé, pas de stores qui s’injectent mutuellement de façon circulaire — cela signale généralement une façade manquante ou un concept mal placé. - Avec SignalStore, seules les méthodes appellent
patchState,rxMethodgère les flux asynchrones (aveccatchErrordans le flux interne) etsignalStoreFeaturesupprime le code de chargement/erreur copié-collé. Les bibliothèques peuvent coexister avec un NgRx Store existant.
On vous demandera probablement ensuite
Deux fonctionnalités ont besoin des mêmes données de panier. Où vit le store, et comment communiquent-elles sans s’importer mutuellement ?
Signal d’alerte
« Toujours NgRx pour tout ce qui est sérieux » ou « un store global pour toute l’application » — l’outillage choisi avant d’avoir compris la propriété et la durée de vie de l’état.
S-05Comment structurer une grosse base de code en code de fonctionnalité et code partagé, et comment faire respecter les frontières dans un monorepo ? Architecture
À dire en premier
Organisez par domaine, pas par type de fichier : les features dépendent de core et de shared, shared ne dépend de rien de spécifique à un domaine, et les features ne s’importent jamais directement entre elles. Les règles qui n’existent que dans un wiki cassent dès la première semaine, donc je les mets dans le lint —
no-restricted-importsdans une seule application, ou des tags Nx avec@nx/enforce-module-boundariesdans un monorepo.Puis allez plus loin
- Des tags sur deux dimensions : scope (qui parle à qui :
scope:shop,scope:shared) × type (sens des dépendances : feature → ui / data-access → util). Les contraintes des deux s’appliquent avec un ET. - Une seule API publique par feature/bibliothèque (un unique barrel à la frontière, jamais dans chaque dossier) ; les imports profonds et les imports statiques de bibliothèques lazy sont des violations.
- La granularité est un compromis : de petites bibliothèques par domaine et par type donnent des graphes
nx affectedétroits et des hits de cache ; une seule énorme bibliothèque shared fait que chaque PR affecte tout le dépôt. - Ne déplacez vers shared que ce qui est générique, stable et déjà utilisé dans plusieurs features (« règle de trois ») ; les composants métier utilisés ailleurs restent dans leur feature propriétaire et sont exportés via son API publique.
'@nx/enforce-module-boundaries': ['error', { depConstraints: [ // scope: who may talk to whom { sourceTag: 'scope:shop', onlyDependOnLibsWithTags: ['scope:shop', 'scope:shared'] }, { sourceTag: 'scope:shared', onlyDependOnLibsWithTags: ['scope:shared'] }, // type: which way dependencies flow { sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:data-access', 'type:util'] }, { sourceTag: 'type:ui', onlyDependOnLibsWithTags: ['type:ui', 'type:util'] }, { sourceTag: 'type:util', onlyDependOnLibsWithTags: ['type:util'] }, // ui never reaches data-access, not even transitively { sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:data-access'] }, ], }],On vous demandera probablement ensuite
Un développeur « corrige » une erreur de lint avec
sourceTag: '*'. Que faites-vous, et comment éviter que cela se reproduise ?Signal d’alerte
Des dossiers par type (
components/,services/) et un fourre-toutshared, avec des frontières « convenues oralement ».- Des tags sur deux dimensions : scope (qui parle à qui :
S-06Comment appliqueriez-vous ports & adapters dans Angular ? Où vit le HTTP, et quand cela n’en vaut-il pas la peine ? Architecture
À dire en premier
Le domaine est du TypeScript pur, les cas d’usage dépendent de ports écrits comme classes abstraites (qui sont aussi des tokens de DI), et le HTTP ne vit que dans des adapters à la périphérie, qui transforment les DTO en entités. Le seul endroit qui lie un port à un adapter est la composition root —
app.config.tsou les providers d’une route. Pour de simples écrans CRUD, c’est excessif.Puis allez plus loin
- Classe abstraite plutôt qu’interface : une interface disparaît à la compilation, il faudrait donc un
InjectionTokenséparé ; la classe abstraite est à la fois le type et le token. - Les erreurs font partie du contrat : l’adapter traduit les timeouts/503 en erreur de domaine, de sorte qu’aucun composant ne voit jamais
HttpErrorResponse. - Le bénéfice se voit dans les tests : règles de domaine en fonctions pures, cas d’usage avec un adapter en mémoire, et une suite de tests de contrat exécutée sur chaque adapter.
- Variante pragmatique : un domaine pur pour les règles qui comptent, un port seulement là où il y a (ou aura) plusieurs implémentations, le reste dans la structure habituelle data-access/ui — et faire respecter les couches avec le lint.
// application/task-repository.ts — the PORT (type + DI token) export abstract class TaskRepository { abstract list(): Promise<readonly Task[]>; abstract save(task: Task): Promise<void>; } // infrastructure/http-task-repository.ts — the ADAPTER (HttpClient + DTO mapping) @Injectable() export class HttpTaskRepository extends TaskRepository { /* … */ } // app.config.ts — the COMPOSITION ROOT providers: [{ provide: TaskRepository, useClass: HttpTaskRepository }]On vous demandera probablement ensuite
Pourquoi
@Injectable({ providedIn: 'root', useClass: HttpTaskRepository })sur le port est-il une mauvaise idée ?Signal d’alerte
Des composants qui injectent
HttpClientou l’adapter concret directement, des DTO qui circulent dans les templates, ou quatre couches pour deux formulaires « à cause de la Clean Architecture ».- Classe abstraite plutôt qu’interface : une interface disparaît à la compilation, il faudrait donc un
S-07Quand choisiriez-vous des micro-frontends avec Native Federation, et quand refuseriez-vous ? Architecture
À dire en premier
Les micro-frontends résolvent un problème d’organisation — des équipes autonomes qui doivent déployer indépendamment et qui se bloquent aujourd’hui à la release — pas un problème technique. Si je ne peux pas nommer deux équipes qui se bloquent mutuellement, je recommande une seule application dans un monorepo avec des routes lazy et des frontières imposées ; les MFE dégradent les performances et ne font que déplacer du code enchevêtré entre dépôts.
Puis allez plus loin
- Raisons valables : de nombreuses équipes avec des cadences différentes, de très grandes organisations, une migration strangler-fig depuis une application legacy, des produits distincts sous une même expérience.
- Angular doit être un singleton avec
strictVersion, idéalement exactement la même version partout : deux runtimes, c’est des composants qui ne peuvent pas entrer dans l’arbre du shell et des services en double instance. Un remote sur une autre version majeure est isolé en Web Component ou en iframe. - Traitez les exposes de chaque remote comme une API publique semver ; communiquez via l’URL, puis des événements typés, puis quelques services singleton partagés — jamais un store global partagé (c’est un monolithe distribué).
- Prévoyez les coûts dès le premier jour : un timeout et un fallback par remote, un manifest par environnement, le nom et la version du remote dans chaque erreur journalisée, des tests e2e sur la combinaison de production.
On vous demandera probablement ensuite
Un remote échoue à se charger en production. Comment le shell doit-il se comporter, pour une route entière par rapport à un widget dans une page ?
Signal d’alerte
« Il nous faut des micro-frontends pour la scalabilité et la performance » ou mettre
strictVersion: falsepour faire disparaître les avertissements de version.S-08Quand Angular Elements est-il le bon outil, et quels sont les pièges de la livraison d’un composant Angular comme custom element ? Composants
À dire en premier
Quand un composant doit vivre en dehors d’une application Angular : widgets de CMS, sites clients, design system pour d’autres frameworks, ou un remote qui ne peut pas partager la version d’Angular.
createCustomElement()transforme les inputs en attributs/propriétés et les outputs enCustomEvent, mais chaque bundle embarque son propre runtime (≈ 33 kB transférés) et il n’y a pas de DI partagée — au sein de la même application Angular, c’est presque toujours le mauvais choix.Puis allez plus loin
- Les attributs sont toujours des chaînes : les inputs numériques/booléens ont besoin de
numberAttribute/booleanAttribute, et les objets/tableaux ne passent que par les propriétés. - Les événements ne remontent pas (bubble) et ne sont pas composed : écoutez sur l’élément lui-même. Utilisez des noms d’output spécifiques (
ratingChange, paschange). - Utilisez
ViewEncapsulation.ShadowDompour que le CSS de la page hôte n’y fuite pas ; thématisez via des variables CSS et::part(). - Regroupez les widgets d’une équipe en un seul bundle (un seul
createApplication(), plusieurs appelsdefine()), utilisez une balise différente du sélecteur du composant, et ne réutilisez pas un élément détruit — créez-en un nouveau.
On vous demandera probablement ensuite
Trois équipes livrent chacune un widget construit séparément sur la même page. Qu’est-ce que cela coûte, et que changeriez-vous ?
Signal d’alerte
Utiliser Angular Elements pour réutiliser des composants entre écrans d’une même application Angular, ou pour du contenu critique pour le SEO qui devrait être rendu côté serveur.
- Les attributs sont toujours des chaînes : les inputs numériques/booléens ont besoin de
S-09Comment choisir entre CSR, SSR et prerender dans une application Angular, et que coûte son exploitation ? SSR
À dire en premier
Par route, pas par projet :
app.routes.server.tsdéfinitRenderMode.Prerender,ServerouClientpour chaque chemin. Prérendez les pages publiques stables, rendez côté serveur les pages publiques aux données fraîches, et gardez côté client la zone derrière le login. Le SSR améliore le LCP et le SEO ; il ne réduit pas le JavaScript et ne corrige pas une application lente après l’hydration.Puis allez plus loin
- Le prerender avec paramètres nécessite
getPrerenderParams;PrerenderFallback.Server(le défaut) permet de prérendre les N premiers produits et de rendre le reste à la demande. Chaque URL est un rendu complet au moment du build. - Le mode Server implique un processus Node : le TTFB est au moins celui de l’API la plus lente appelée pendant le rendu, du CPU par requête, et un état au niveau des modules partagé entre utilisateurs.
- Les bons codes de statut comptent pour le SEO : définissez le 404 via
RESPONSE_INITau lieu d’un soft 404 ; le cache se règle par route —public, s-maxageuniquement pour un contenu identique,private, no-storepour du HTML personnel. - En v22, configurez
allowedHosts(ouNG_ALLOWED_HOSTS) : un hôte inconnu reçoit un 400 en protection contre le SSRF. Si personne ne peut exploiter un serveur Node, utilisez le Prerender avecoutputMode: 'static'.
On vous demandera probablement ensuite
Vos pages SSR sont devenues lentes après la dégradation d’une API backend. Pourquoi, et que changez-vous ?
Signal d’alerte
« Activez le SSR partout, ça rend l’application plus rapide » — en ignorant que le TTI change à peine, le coût serveur, et les pages personnelles mises en cache publiquement au CDN.
- Le prerender avec paramètres nécessite
S-10Expliquez l’incremental hydration et l’event replay en v22. Comment les utilisez-vous sur une page produit ? SSR
À dire en premier
En v22, un simple
provideClientHydration()active l’incremental hydration, et avec elle l’event replay. Les blocs@defer (hydrate on …)sont rendus côté serveur mais restent déshydratés dans le navigateur — leur JS n’est même pas téléchargé — jusqu’au déclenchement ; les clics effectués avant sont mis en file puis rejoués, et un clic dans un bloc déshydraté l’hydrate.Puis allez plus loin
- Contrairement à un
@deferordinaire, le contenu final est déjà à l’écran, donc vous pouvez différer au-dessus de la ligne de flottaison sans décalage de mise en page. hydrate neverest réservé au contenu vraiment statique : il n’est pas annoté, donc rien n’est rejoué. Gardez le@placeholder: les déclencheurs hydrate sont ignorés lors d’une navigation côté client.withIncrementalHydration()est déprécié etwithEventReplay()est redondant avec le défaut ; pour s’en désengager, on utilisewithNoIncrementalHydration().- N’enveloppez pas des composants de quelques Ko : le chunk et la requête supplémentaires coûtent plus qu’ils n’économisent. Utilisez des états CSS (
:active,:focus-visible) pour que les utilisateurs voient que le clic a été pris en compte.
<app-product-header [product]="product()" /> <!-- hydrated at bootstrap --> @defer (hydrate on interaction) { <app-gallery [images]="product().images" /> <!-- JS only at the first click --> } @placeholder { <div class="gallery-skeleton"></div> <!-- shown on client-side navigation --> } @defer (hydrate on viewport) { <app-reviews [productId]="id()" /> <!-- below the fold --> } @placeholder { <div class="reviews-skeleton"></div> } @defer (hydrate never) { <app-footer /> } <!-- static HTML forever -->On vous demandera probablement ensuite
Qu’exige
hydrate whendu parent, et que se passe-t-il avec des blocs déshydratés imbriqués ?Signal d’alerte
Mettre
hydrate neversur quelque chose qui a des listeners, ou supprimer@placeholder« parce qu’il n’est jamais visible ».- Contrairement à un
S-11Quel code n’est pas compatible SSR, et comment éviter les hydration mismatches dans une équipe ? SSR
À dire en premier
Tout ce qui touche à
window,localStorage, à la mise en page ou aux observers dans les constructeurs, les initialiseurs de champs,ngOnInitoueffect()— cela s’exécute aussi côté serveur. Le travail réservé au navigateur va dansafterNextRender/afterRenderEffectou derrière un provider de plateforme, et le rendu produit doit être identique sur les deux plateformes.Puis allez plus loin
@if (isPlatformBrowser(…))dans un template ne lève pas d’erreur, mais Angular jette le DOM serveur de ce bloc et le reconstruit — un décalage de mise en page. Préférez@deferouafterNextRender.- Causes de mismatch : manipulation directe du DOM (
innerHTML,appendChild), HTML invalide (<div>dans<p>), scripts tiers, un CDN qui supprime les commentaires (NG0507), et des valeurs commeDate.now()qui diffèrent entre plateformes. - Les serveurs zoneless ne voient pas votre asynchrone maison (fetch, SDK, timers) : utilisez
resource()ouPendingTasks, sinon le HTML part vide et le navigateur redemande les données. - Fuites d’état : les variables au niveau des modules et les objets
useValuesont partagés par tous les utilisateurs du processus Node ; utilisez des services ouuseFactory. - En équipe : un build SSR en CI, une checklist de relecture, et
ngSkipHydrationautorisé uniquement comme correctif documenté et temporaire.
On vous demandera probablement ensuite
La préférence de thème est stockée dans
localStorage. Comment rendre le bon thème côté serveur ?Signal d’alerte
Des
if (typeof window !== 'undefined')semés dans les composants, ou considérereffect()comme un « hook réservé au navigateur ».S-12Vous héritez d’une application Angular lente. Quelle est votre stratégie de performance, et comment éviter les régressions ? Performance
À dire en premier
Mesurer d’abord, puis corriger le plus gros goulot d’étranglement : les Core Web Vitals (LCP, INP, CLS) issus d’utilisateurs réels et le Profiler d’Angular DevTools me disent si le problème vient du chargement, du rendu ou d’un thread principal saturé. Ensuite je verrouille les gains avec des budgets de bundle en CI.
Puis allez plus loin
- Chargement : routes lazy,
@deferpour le contenu lourd sous la ligne de flottaison (jamais l’élément LCP),injectAsync()pour les services lourds et rarement utilisés,NgOptimizedImageavecprioritysur l’image LCP, imports ciblés vérifiés avecsource-map-explorer. - Rendu : OnPush + signaux,
tracksur un identifiant stable, dérivations danscomputed()plutôt que des méthodes de template ; pour des dizaines de milliers de lignes, le virtual scroll du CDK ou la pagination — OnPush réduit la fréquence, pas la quantité que vous rendez. - Thread principal : les calculs continus de plus de ~50 ms (parsing, gros filtres) vont dans un Web Worker ; transférez des
ArrayBufferau lieu de copier d’énormes données. - Contrôle des régressions : des
budgetsdansangular.jsonqui font échouer le build, une vérification de performance des pages clés à chaque release, et un responsable des chiffres.
On vous demandera probablement ensuite
L’INP est mauvais mais le LCP est bon. Où regardez-vous en premier ?
Signal d’alerte
Optimiser sans mesurer, ou « ajouter du SSR et du
@deferpartout » — y compris sur le contenu principal, ce qui dégrade le LCP.- Chargement : routes lazy,
S-13Comment Angular protège-t-il contre le XSS, où sont les portes dérobées, et quelle est votre seconde ligne de défense ? Sécurité
À dire en premier
Angular considère toute valeur liée comme non sûre : l’interpolation échappe,
[innerHTML]est assaini, les schémas d’URL dangereux deviennentunsafe:. Le XSS dans les applications Angular vient presque toujours du contournement de cela —bypassSecurityTrust*sur des données non fiables,nativeElement.innerHTML,eval— donc la seconde ligne est une CSP stricte avec un nonce, plus Trusted Types.Puis allez plus loin
bypassSecurityTrust*n’est légitime que pour du contenu que vous construisez et contrôlez (une URL d’embed issue d’un identifiant validé). Une pipe générique « safeHtml » est un XSS réutilisable ; cherchez ces appels à chaque relecture.- CSP : Angular a besoin d’un nonce pour ses styles inline —
ngCspNoncesur l’élément racine ou le tokenCSP_NONCE;security.autoCspcalcule les hashes au build mais ne couvre que les scripts et ne fonctionne pas avec le SSR. - Déployez la CSP d’abord avec
Content-Security-Policy-Report-Only, sinon les chunks lazy ou les styles inline cassent en production. - CSRF avec cookies de session :
HttpClientenvoie le cookieXSRF-TOKENsous la formeX-XSRF-TOKENsur les requêtes mutantes de même origine — mais le serveur doit le définir et le vérifier. Chaque vérification côté frontend est répétée côté serveur.
On vous demandera probablement ensuite
Le produit veut afficher du Markdown écrit par les utilisateurs. Comment le faire en toute sécurité ?
Signal d’alerte
« Angular assainit tout, donc nous sommes en sécurité » alors que la base de code contient une pipe
bypassSecurityTrustHtmlappliquée à du contenu de CMS.S-14Concevez un interceptor d’authentification avec des refresh tokens. Que se passe-t-il quand six requêtes reçoivent un 401 en même temps, et où stockez-vous les jetons ? HTTP
À dire en premier
Exactement un refresh en cours, partagé par toutes les requêtes en attente (un Observable mis en cache avec
shareReplay(1), vidé dansfinalize), puis chaque requête d’origine est rejouée avec le nouveau jeton. Access token en mémoire, refresh token dans un cookie HttpOnly, Secure, SameSite — ou un BFF, où le navigateur n’a qu’un cookie de session.Puis allez plus loin
- Le « refresh à chaque 401 » naïf produit N refreshes ; avec la rotation des refresh tokens, ils s’invalident mutuellement et certains serveurs traitent la réutilisation comme une attaque et tuent la session.
- Marquez le login/refresh avec un
HttpContextToken(SKIP_AUTH) pour que l’interceptor ne s’intercepte pas lui-même, n’attachez le jeton qu’aux URL de votre API, et si le jeton a déjà changé depuis l’envoi de la requête, réessayez simplement. - Seul le 401 déclenche un refresh — le 403 signifie « non autorisé ». Un refresh qui échoue entraîne une déconnexion, le vidage des stores et des caches, et la propagation de la déconnexion aux autres onglets (
BroadcastChannel). - Au démarrage, un
provideAppInitializertente un refresh avant la première route. Avec un fournisseur OIDC externe, utilisez une bibliothèque certifiée plutôt que de réimplémenter PKCE et la rotation à la main.
private refreshInFlight: Observable<string> | null = null; refresh(): Observable<string> { this.refreshInFlight ??= this.http .post<{ accessToken: string }>('/auth/refresh', null, { withCredentials: true, // HttpOnly cookie context: new HttpContext().set(SKIP_AUTH, true), // not intercepted again }) .pipe( map((r) => r.accessToken), tap((t) => this.token.set(t)), // access token: memory only finalize(() => (this.refreshInFlight = null)), shareReplay(1), // every 401 waits for the same one ); return this.refreshInFlight; }On vous demandera probablement ensuite
Pourquoi
shareReplay(1)est-il acceptable ici, alors que c’est un piège connu sur d’autres flux ?Signal d’alerte
Un JWT dans
localStoragesans réfléchir au XSS ou à la CSP, un refresh distinct pour chaque 401, ou des contrôles de permissions fondés sur le décodage du JWT côté frontend.S-15Quelle est votre stratégie de gestion des erreurs dans une application Angular ? Architecture
À dire en premier
En couches : les erreurs prévisibles sont gérées localement près des données (un 404 ou un 422 affiché par le store/composant), les erreurs transverses de façon centralisée dans un interceptor (401, 5xx), les erreurs de rendu d’une zone avec
@boundary(v22.2, developer preview), et tout le reste atteint unErrorHandlerglobal qui journalise vers la télémétrie.Puis allez plus loin
provideBrowserGlobalErrorListeners()acheminewindow.onerroret les rejets non gérés versErrorHandler; gardez le handler simple et sûr — s’il lève une erreur, vous perdez toutes les erreurs.@boundaryn’intercepte que les erreurs de rendu, pas celles des gestionnaires d’événements ni du code asynchrone, et une erreur interceptée est quand même signalée àErrorHandler.- L’interceptor relance après traitement (
throwError(() => err)) pour que les appelants puissent encore réagir ; uncatchError(() => of([]))sans journalisation rend les bugs invisibles. - UX et exploitation : centralisez et dédupliquez les toasts (pas cinq messages pour un problème), traduisez les erreurs techniques en erreurs de domaine au niveau de l’adapter, et joignez du contexte (route, version, remote) à chaque erreur journalisée.
On vous demandera probablement ensuite
Un widget du tableau de bord lève une erreur pendant le rendu et blanchit toute la page. Comment l’isolez-vous ?
Signal d’alerte
Un
try/catchoucatchErrordans chaque composant qui avale les erreurs, et un toast pour chacune, sans journalisation centrale.S-17Quelle stratégie de test fixeriez-vous pour une équipe : que met-on dans les tests unitaires, de composants et e2e, et que mocke-t-on ? Tests
À dire en premier
Testez le comportement, pas l’implémentation, et poussez chaque vérification au niveau le moins coûteux qui peut la détecter : fonctions pures et stores sans le DOM, composants dumb via leurs inputs et outputs, pages smart avec un faux store et
HttpTestingController, et quelques parcours e2e (Playwright/Cypress) pour ce que seule l’intégration révèle. Mockez aux frontières — HTTP et ports — pas vos propres composants enfants.Puis allez plus loin
- Depuis la v21, le runner par défaut est Vitest (
@angular/build:unit-test) ; Karma est déprécié, et les anciennes suites peuvent être migrées avec le schematic expérimentalrefactor-jasmine-vitest. - Tests zoneless :
await fixture.whenStable()au lieu defakeAsync, fake timers de Vitest pour le debounce,TestBed.tick()pour vider les effects ; inputs sous forme de signaux viainputBinding. - Avec ports & adapters, exécutez une suite de tests de contrat sur chaque adapter ; les harnesses du CDK rendent les tests de composants indépendants de la structure du DOM.
- En CI :
nx affectedplus le cache pour la vitesse, pas de cache pour les e2e non déterministes contre un vrai backend, et la couverture comme signal, pas comme objectif.
On vous demandera probablement ensuite
Un test e2e instable échoue une fois sur dix. Comment le traitez-vous en équipe ?
Signal d’alerte
« 100 % de couverture avec tout mocké » — des tests qui vérifient que des méthodes privées ont été appelées, cassent à chaque refactor et passent à côté de vrais bugs.
- Depuis la v21, le runner par défaut est Vitest (
S-18Quelle stratégie d’i18n choisiriez-vous : l’i18n d’Angular au build ou une bibliothèque de traduction à l’exécution ? i18n
À dire en premier
@angular/localizeintégré par défaut : textes marqués aveci18n/$localize, extraits avecng extract-i18n, et un build optimisé par locale sans recherche de clé à l’exécution. Une bibliothèque d’exécution seulement quand changer de langue sans rechargement est une exigence produit, ou que les traductions viennent d’un CMS sans déploiement.Puis allez plus loin
- Changer de langue avec l’i18n intégrée implique un rechargement, même avec
loadTranslations()à l’exécution, car les textes des templates sont évalués à la création des composants. - Les pluriels exigent l’ICU, pas
n === 1 ? … : …: le roumain aone,fewetother(« 20 de fișiere »). Ne concaténez jamais des phrases — l’ordre des mots diffère d’une langue à l’autre. - Des ids personnalisés (
@@cart.title) pour que les traductions survivent aux modifications de la source ; dates, nombres et devises via des pipes avecLOCALE_IDetregisterLocaleData. - Il s’adapte au SSR/prerender (une variante par locale, choisie par l’URL), et avec l’hydration vous ajoutez
withI18nSupport(). Commencez dès le premier jour : ajouter l’i18n plus tard, c’est traquer chaque chaîne codée en dur.
On vous demandera probablement ensuite
Comment organisez-vous le workflow avec des traducteurs professionnels et gardez-vous les fichiers XLIFF synchronisés d’une release à l’autre ?
Signal d’alerte
Des formats codés en dur comme
'dd/MM/yyyy'outoFixed(2) + ' lei', la concaténation de chaînes pour les phrases, et aucun id de message personnalisé.- Changer de langue avec l’i18n intégrée implique un rechargement, même avec
S-19Votre entreprise construit son propre design system en Angular. Comment faire de l’accessibilité une propriété du système plutôt que de chaque écran ? Accessibilité
À dire en premier
Construisez les patterns interactifs sur Angular Aria (
@angular/aria, depuis la v21) : des directives headless comme Tabs, Menu, Combobox, Listbox, Tree ou Grid qui fournissent les bons rôles ARIA, la navigation au clavier et la gestion du focus sans aucun style, pendant que l’équipe garde la main sur le CSS. Ainsi chaque équipe produit obtient l’accessibilité par défaut au lieu de réimplémenter à la main des onglets et des menus déroulants.Puis allez plus loin
- Choix de la couche : Aria pour un design system sur mesure, CDK pour les primitives de bas niveau (overlay, drag & drop, utilitaires a11y), Material quand on veut rapidement une UI toute faite.
- Stylez selon l’état ARIA que posent les directives (
[aria-selected='true']), pour que l’état visuel et l’état accessible ne puissent pas diverger. - Compromis ailleurs : le virtual scroll retire les lignes hors écran du DOM (Ctrl+F et les lecteurs d’écran ne les voient pas) ; préférez la pagination là où l’accessibilité complète compte.
- Processus : vérifications au clavier et au lecteur d’écran dans la definition of done, contrôles automatisés en CI, et revue a11y intégrée au contrat du design system.
On vous demandera probablement ensuite
Une équipe utilise déjà Angular Material. La migreriez-vous vers Aria ?
Signal d’alerte
« On ajoute les attributs
aria-*à la fin, avant la release » — l’accessibilité traitée comme une checklist finale plutôt que comme une propriété des composants.S-20Vous pilotez la mise à niveau d’une grosse application d’Angular 21 vers 22. Quels breaking changes vous inquiètent, et comment déployez-vous ? Architecture
À dire en premier
Une version majeure à la fois avec
ng update @angular/core@22 @angular/cli@22, en laissant les migrations s’exécuter — p. ex. elles ajoutentChangeDetectionStrategy.Eagerlà où les composants n’avaient pas de stratégie, afin que le comportement reste identique alors qu’OnPush devient le défaut. Le vrai risque se trouve dans les changements d’exécution que les migrations ne voient pas entièrement : le backend fetch, l’héritage des paramètres du routeur et la sémantique des templates.Puis allez plus loin
- HTTP : fetch est le backend par défaut et
withFetch()est déprécié ; la progression d’upload nécessitewithXhr(), etreportProgressdevientreportUploadProgress/reportDownloadProgress. - Routeur et templates :
paramsInheritanceStrategy: 'always'(les routes enfants héritent des paramètres et des données de tous les parents),?.renvoieundefinedcomme en TypeScript, un compilateur plus strict (inputs liés deux fois,data-*ne lie plus d’inputs),CanMatchFnnécessitecurrentSnapshot. - Plateforme et suppressions : TypeScript 6 et pas de Node 20 ;
ComponentFactoryResolver,ComponentFactoryetprovideRoutesont disparu ; pour le SSR, configurezallowedHostset retirezwithIncrementalHydration(). - Déploiement : une branche de mise à niveau maintenue au vert en CI, bibliothèques et Node/TS mis à jour d’abord, e2e sur les parcours critiques, puis un plan de suivi pour retirer
Eagerfonctionnalité par fonctionnalité au lieu de le traiter comme permanent.
On vous demandera probablement ensuite
Après la mise à niveau, une route imbriquée reçoit soudain un paramètre qu’elle ne recevait pas avant. Qu’est-ce qui a changé, et comment vérifier si cela compte ?
Signal d’alerte
Modifier les versions de
package.jsonà la main et sauterng update, ou enjamber plusieurs versions majeures d’un coup sans lire le changelog.- HTTP : fetch est le backend par défaut et
Comment bien répondre
- Une phrase claire d’abord, puis les détails seulement si on vous les demande.
- Un exemple concret tiré d’un projet : « on avait X, on a choisi Y parce que Z ».
- Nommer le compromis et le cas où vous choisiriez autrement.
- « Je ne sais pas, mais voici comment je le découvrirais » quand vous ne savez vraiment pas.
- Réciter des définitions sans dire quand vous les utiliseriez.
- Présenter de vieilles réponses comme actuelles (des NgModules partout, Zone.js comme une évidence).
- « Ça dépend » sans dire de quoi.
- Inventer une API pour éviter le silence.