60 de întrebări care chiar apar la interviurile de Angular în 2026, împărțite pe nivel: ce trebuie să știe un junior, ce se așteaptă de la un mid care face singur feature-uri și ce e întrebat un senior despre mecanisme interne, arhitectură și compromisuri. Fiecare răspuns începe cu ce spui întâi, apoi ce adaugi ca să arăți că înțelegi, întrebarea care urmează de obicei și răspunsul care pică interviul.
Cum folosești pagina
- Răspunde cu voce tare înainte să deschizi răspunsul. Când citești ai impresia că știi; la interviu va trebui să o spui.
- Începe cu răspunsul scurt, apoi adaugă una-două idei din „detalii”. Intervievatorii preferă o poziție clară în locul unei liste lungi.
- Marchează ce știi cu „Știu asta”. Se salvează în acest browser, iar simularea îți dă întâi restul.
- Fiecare răspuns are link către lecția care explică subiectul pe larg.
Simulare de interviu
Câteva întrebări alese la întâmplare, pe rând, cu răspunsul ascuns până îl ceri. La final primești lista întrebărilor pe care merită să le recitești.
Răspunde cu voce tare, ca și cum te-ar asculta cineva. Apoi deschide răspunsul model și fii sincer cu tine.
Junior: fundamentele
La primul job de Angular nu se așteaptă să știi mecanismele interne, dar se așteaptă să știi fără ezitare bazele moderne: componente standalone, noul control flow, semnale, input-uri și output-uri, servicii cu inject(), routing și formulare. Arată că știi de ce, nu doar cum.
J-01Ce este o componentă și ce înseamnă „standalone”? Cum pornește o aplicație fără NgModule? Componente
Spune asta întâi
O componentă este o clasă TypeScript cu
@Componentcare leagă un template de stare și comportament și se folosește prin selector. Standalone (implicit) înseamnă că își declară singură dependențele înimports, deci nu mai e nevoie de NgModule: aplicația pornește cubootstrapApplication(App, appConfig).Apoi intră în detalii
- Nu mai scrii
standalone: true— e implicit. Fiecare componentă importă exact ce folosește template-ul (alte componente, pipe-uri,RouterLink), deci dependențele sunt explicite și tree-shakable. - Configurarea la nivel de aplicație stă în
app.config.ts, caproviders:provideRouter(routes),provideHttpClient(...)doar dacă îl configurezi etc. - O bibliotecă veche care exportă doar un NgModule poate fi pusă direct în
importsla componentă; providerii de modul trec prinimportProvidersFrom().
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));Probabil te întreabă apoi
Unde pui un provider de care are nevoie toată aplicația?
Semnal de alarmă
Spune că orice componentă trebuie declarată într-un
AppModulesau importăCommonModule„ca să fie”.- Nu mai scrii
J-02Ce tipuri de data binding există într-un template? Template-uri
Spune asta întâi
Interpolarea
{{ value }}și property binding[prop]="expr"merg de la componentă spre DOM; event binding(event)="handler($event)"merge de la DOM spre componentă; two-way binding[(value)]="sig"le combină pe amândouă.Apoi intră în detalii
[disabled]leagă o proprietate DOM; pentru ce există doar ca atribut (ARIA,colspan) folosești[attr.aria-label].[(x)](„banana in a box”) e doar zahăr sintactic pentru[x]="v()"+(xChange)="v.set($event)". Cu un semnal pasezi semnalul însuși, fără paranteze.- Expresiile din template trebuie să fie ieftine și fără efecte secundare; citirea unui semnal ca
count()e în regulă, o metodă grea rulează la fiecare verificare.
<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" />Probabil te întreabă apoi
Care e diferența dintre un atribut și o proprietate?
Semnal de alarmă
Crede că
[value]="x"șivalue="x"sunt același lucru (al doilea e un string static) sau scrie[(checked)]="notifications()".J-03Cum scrii condiții și bucle în template și de ce are nevoie
@fordetrack? Template-uriSpune asta întâi
Cu control flow-ul integrat:
@if/@else,@forcu@emptyși@switch/@case.trackeste obligatoriu: îi spune lui Angular cum identifică fiecare element, ca să refolosească nodurile DOM în loc să recreeze toată lista.Apoi intră în detalii
- Faci track după un id stabil și unic (
track user.id);$indexdoar pentru liste statice; track pe obiectul însuși înseamnă că fiecare refetch distruge și recreează DOM-ul. - Face parte din limbaj: fără importuri de
NgIf/NgFor.@forexpune$index,$first,$last,$even,$odd,$count. @if (user(); as u)dă un alias tipizat, non-null. Din v21.1 mai multe@casepot împărți un bloc, iar@default never;(v21.2) face dintr-un caz uitat al unui union o eroare de compilare.
@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> }Probabil te întreabă apoi
Ce se întâmplă cu DOM-ul dacă faci track după
$indexși apoi sortezi lista?Semnal de alarmă
Folosește în continuare
*ngIf/*ngForîn cod nou sau nu poate explica la ce foloseștetrack.- Faci track după un id stabil și unic (
J-04Ce este
@letși când l-ai folosi? Template-uriSpune asta întâi
@letdeclară o variabilă locală, read-only, în template, vizibilă în blocul curent și în copiii lui. Îl folosesc ca să dau nume unei expresii lungi folosite de mai multe ori sau ca să despachetez o valoare o singură dată, de ex.@let user = user$ | async;.Apoi intră în detalii
- Nu poate fi reatribuit din template; se recalculează când se schimbă ce citește.
- E util în
@forpentru o valoare per element (de ex. prețul final după reducere). - Logica reală rămâne în clasă, ca
computed();@lete pentru lizibilitate, nu pentru reguli de business.
Semnal de alarmă
Folosește
@letca pe o variabilă mutabilă sau mută cu el logică de business complexă în template.J-05Cum adaugi condiționat o clasă CSS sau un stil? Template-uri
Spune asta întâi
Cu binding-uri native:
[class.active]="isActive()"pentru o clasă,[class]="{ ... }"pentru mai multe și[style.width.px]="width()"pentru stiluri. Nu au nevoie de import, iar clasele statice se combină cu cele din binding, nu se suprascriu.Apoi intră în detalii
[class]acceptă string, array sau obiect;[style]acceptă string sau obiect, iar unitățile se pun în binding (.px,.%,.rem).[ngClass]/[ngStyle]sunt varianta veche; v21 are migrărilengclass-to-classșingstyle-to-style.- Pentru elementul gazdă al componentei folosești metadata
host('[class.is-loading]': 'loading()'), nu@HostBinding.
Probabil te întreabă apoi
Cum ai seta o variabilă CSS dintr-un semnal?
Semnal de alarmă
Construiește manual string-uri de clase în componentă sau umblă direct la
classListpe element.J-06Ce este un pipe și ce înseamnă „pure”? Template-uri
Spune asta întâi
Un pipe transformă o valoare pentru afișare direct în template, ca
{{ price | currency }}. Implicit e pure: Angular îl rulează din nou doar când se schimbă valoarea de intrare sau argumentele (prin referință), deci e memoizat.Apoi intră în detalii
- Pipe-urile sunt standalone și se importă ca orice dependență (
imports: [DatePipe]); unul custom e o clasă cu@Pipeșitransform(). - Pentru că verificarea e prin referință, un pipe pure nu rulează din nou dacă modifici un array pe loc.
- O metodă apelată din template rulează la fiecare verificare; pentru calcule specifice componentei preferă
computed(), iar pipe-urile păstrează-le pentru formatare reutilizabilă. Evităpure: false.
Probabil te întreabă apoi
Ai scrie un pipe sau un
computed()ca să filtrezi o listă?Semnal de alarmă
Face impur un pipe de filtrare ca „să meargă” pe o listă mare.
- Pipe-urile sunt standalone și se importă ca orice dependență (
J-07Cum trimite un părinte date unei componente copil și cum îi răspunde copilul? Componente
Spune asta întâi
Copilul declară
input()pentru datele care intră șioutput()pentru evenimentele care ies. Părintele leagă[value]="..."și(rate)="onRate($event)"; copilul apeleazărate.emit(4).Apoi intră în detalii
- Un
input()este un semnal read-only: îl citești cuvalue()și derivi din el cucomputed()— fărăngOnChanges. input.required<T>()face dintr-un binding lipsă o eroare de compilare;transform: numberAttribute/booleanAttributetransformămax="10"sau un simplu atributreadonlyîn tipul corect.- Copilul nu poate face
set()pe propriul input. Dacă trebuie să schimbe valoarea, emite un eveniment și lasă părintele să decidă sau foloseștemodel().
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), ); }Probabil te întreabă apoi
Ce se schimbă dacă copilul trebuie să modifice valoarea pe care o primește?
Semnal de alarmă
Spune că ai nevoie de
ngOnChangesca să reacționezi la un input sau folosește@Input/EventEmitterîn cod nou.- Un
J-08Ce este
model()și prin ce diferă deinput()+output()? ComponenteSpune asta întâi
model()este un semnal writable care funcționează ca un input plus un output automatxChange, deci părintele poate folosi two-way binding:[(checked)]="notifications". Când copilul apeleazăset()sauupdate(), semnalul părintelui se actualizează.Apoi intră în detalii
- Folosește
model()când valoarea aparține cu adevărat ambelor părți, ca la un toggle, un stepper sau un input custom. - Folosește
input()+output()când copilul doar propune, iar părintele decide (validează, confirmă, salvează pe server). - În two-way binding pasezi semnalul însuși (
notifications), nu valoarea lui (notifications()).
Semnal de alarmă
Nu poate explica în ce se desface
[( )]sau foloseștemodel()pentru orice.- Folosește
J-09Ce este content projection? Componente
Spune asta întâi
Îi permite părintelui să pună propriul markup între tag-urile unei componente, iar componenta decide unde îl afișează cu
<ng-content />. Cuselectpoți avea mai multe sloturi, de exemplu un titlu și acțiuni într-un card.Apoi intră în detalii
selectprimește un selector de tag, atribut sau clasă ([card-title]);<ng-content />fărăselectprinde tot restul, iar în interior poți pune conținut fallback.- Capcană: conținutul proiectat e creat de părinte chiar dacă copilul îl ascunde cu
@if. Pentru conținut cu adevărat lazy, pasezi un<ng-template>. - Proiecția e pentru markup flexibil (layout-uri, carduri, dialoguri); input-urile sunt pentru date pe care componenta le formatează singură.
Probabil te întreabă apoi
Când ai alege proiecția în locul unui input?
Semnal de alarmă
Trimite HTML ca string printr-un input și îl afișează cu
innerHTMLîn loc să-l proiecteze.J-10Ce sunt
signal()șicomputed()? SignalsSpune asta întâi
Un
signal()este o valoare reactivă: o citești apelând-o și o schimbi cuset()sauupdate(). Uncomputed()este o valoare read-only derivată din alte semnale; e lazy și memoizat, recalculat doar când i se schimbă dependențele.Apoi intră în detalii
- Dependențele sunt urmărite automat: ce semnale citește funcția devin dependențe, fără abonamente de gestionat.
- Semnalele compară cu
===:items().push(x)nu anunță pe nimeni; foloseșteupdate((list) => [...list, x]). - Regulă practică: tot ce se poate calcula din altă stare e un
computed(), nu un al doilea semnal ținut sincronizat. Expui starea cuasReadonly().
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() }}Probabil te întreabă apoi
De ce nu se actualizează UI-ul după
items().push(x)?Semnal de alarmă
Încearcă
set()pe uncomputed()sau uită parantezele și leagă funcția semnalului în loc de valoarea lui.J-11Ce este
effect()și când nu ar trebui să-l folosești? SignalsSpune asta întâi
effect()rulează o funcție o dată, apoi din nou de fiecare dată când se schimbă semnalele pe care le citește. E pentru efecte secundare spre lumea exterioară —localStorage, logging, o bibliotecă terță — nu pentru calcularea stării.Apoi intră în detalii
- Nu folosi un effect ca să copiezi un semnal în altul: asta e un
computed()(saulinkedSignal()dacă trebuie să fie și editabil). - Nu încărca date dintr-un effect: nu ai anulare, iar răspunsurile vechi pot câștiga cursa. Folosește
httpResource()/resource(). - Trebuie creat într-un context de injecție (constructor sau câmp), se distruge odată cu componenta, iar
onCleanupcurăță timerele înainte de următoarea rulare.
Probabil te întreabă apoi
Ce se întâmplă dacă creezi un effect în
ngOnInit?Semnal de alarmă
Folosește
effect()ca să ținăfullNamesincronizat cufirstNameșilastName.- Nu folosi un effect ca să copiezi un semnal în altul: asta e un
J-12Care e diferența dintre constructor și
ngOnInit? Cum faci curățenie când o componentă e distrusă? ComponenteSpune asta întâi
Constructorul rulează la crearea clasei și este contextul de injecție:
inject(), semnalele și efectele stau acolo sau în inițializarea câmpurilor.ngOnInitrulează o dată, după ce sunt setate primele input-uri. Pentru curățenie folosescinject(DestroyRef).onDestroy()sautakeUntilDestroyed().Apoi intră în detalii
- Citirea unui
input.required()în constructor aruncă eroare, pentru că părintele nu l-a legat încă. Cu input-uri semnal rar ai nevoie dengOnInit: uncomputed()sauhttpResource()citește input-ul când e gata. inject()nu merge înngOnInitsau într-un handler de click — e în afara contextului de injecție (eroarea NG0203).DestroyRefmerge și în servicii, directive și funcții reutilizabile, ceea cengOnDestroynu poate. Pentru acces la DOM după randare, foloseșteafterNextRender().
export class Clock { readonly now = signal(new Date()); constructor() { const id = setInterval(() => this.now.set(new Date()), 1000); inject(DestroyRef).onDestroy(() => clearInterval(id)); } }Probabil te întreabă apoi
Unde ai inițializa o bibliotecă de grafice care are nevoie de elementul DOM?
Semnal de alarmă
Spune că constructorul e „doar pentru DI” și pune toată logica în
ngOnInitsau nu curăță niciodată intervalele și abonamentele.- Citirea unui
J-13Ce este un serviciu și cum îl folosești într-o componentă? DI
Spune asta întâi
Un serviciu este o clasă care ține logică sau stare partajată între componente — apeluri API, un coș, utilizatorul logat. În v22 îl marchez cu
@Service(), care e furnizat în root ca singleton, și îl obțin cuinject(CartService).Apoi intră în detalii
@Service()este echivalent cu@Injectable({ providedIn: 'root' }): creat la prima injecție și tree-shakable. Cereinject()— DI prin constructor e eroare de compilare.@Injectablerămâne valid (cod existent, biblioteci pentru versiuni mai vechi). Dacă pui serviciul înprovidersla o componentă, primești câte o instanță per componentă, nu una pe aplicație.inject()merge și în guards, resolvers și interceptors funcționale și evită apelurile lungisuper(...)la moștenire.
@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]); } }Probabil te întreabă apoi
Cum ai da fiecărei instanțe a unei componente wizard propria copie a unui serviciu?
Semnal de alarmă
Creează serviciul cu
new CartService()sau crede că un serviciu pus înprovidersrămâne același singleton global.J-14Cum configurezi routing-ul și cum navighezi între pagini? Routing
Spune asta întâi
Definești un array
Routes, îl înregistrezi cuprovideRouter(routes)și pui<router-outlet />unde se afișează paginile. Navighezi curouterLinkîn template și curouter.navigate()în cod; paginile se încarcă de obicei lazy culoadComponent.Apoi intră în detalii
loadComponent: () => import('./home-page')pune pagina într-un chunk separat, descărcat doar când e vizitată ruta;loadChildrenface la fel pentru un feature întreg.- Ordinea contează — câștigă prima potrivire — deci wildcard-ul
'**'stă la final.routerLinkActiveadaugă o clasă pe link-ul activ. - Folosește
routerLink, nuhref:hrefreîncarcă toată aplicația.
Probabil te întreabă apoi
Cum ai proteja o rută astfel încât s-o poată deschide doar utilizatorii logați?
Semnal de alarmă
Navighează cu
<a href>sauwindow.locationori pune ruta wildcard prima.J-15Cum citește o pagină un parametru de rută ca
:id? RoutingSpune asta întâi
Cu
withComponentInputBinding()înprovideRouter, parametrii de cale, query params și data rutei ajung ca input-uri ale componentei:readonly id = input.required({ transform: numberAttribute }). Fiind semnal, tot ce derivă din el se actualizează când se schimbă id-ul.Apoi intră în detalii
- Capcană: navigarea de la
/products/1la/products/2refolosește aceeași instanță a componentei; codul care citește id-ul o singură dată (de ex. un snapshot înngOnInit) afișează date vechi. - Parametrii sunt string-uri în URL, deci un transform ca
numberAttributeîți dă un număr real. - Din v22 rutele copil moștenesc implicit params și data de la toți părinții (
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()); }Probabil te întreabă apoi
Ce se strică dacă citești id-ul o singură dată cu
route.snapshot?Semnal de alarmă
Știe doar
route.snapshot.paramMap.get('id')și nu știe că instanța componentei poate fi refolosită.- Capcană: navigarea de la
J-16Cum apelezi un API REST în Angular? HTTP
Spune asta întâi
Cu
HttpClient, injectat într-un serviciu:http.get<Product[]>('/api/products')întoarce un Observable. Din v21 e disponibil în root fără setup;provideHttpClient()e doar pentru configurare, de exemplu interceptors.Apoi intră în detalii
- Observable-ul e cold: nicio cerere nu pleacă până nu se abonează cineva (
subscribe(),firstValueFrom(), pipe-ul async). - Pentru citiri reactive într-o componentă,
httpResource()dăvalue(),isLoading()șierror()ca semnale și anulează cererile vechi; pentru scrieri (POST, PUT) apeleziHttpClient. - Genericul
get<T>e doar o aserțiune de tip, nu validare. În v22 Fetch e backend-ul implicit; preocupările transversale (token, erori) merg în interceptors funcționale.
Probabil te întreabă apoi
Unde ai adăuga un header
Authorizationla fiecare cerere?Semnal de alarmă
Apelează
http.get()fără să se aboneze și se miră că nu se întâmplă nimic sau apeleazăfetchdirect din componente.- Observable-ul e cold: nicio cerere nu pleacă până nu se abonează cineva (
J-17Cum construiești un formular cu validare în Angular 22? Formulare
Spune asta întâi
Pentru formulare noi folosesc Signal Forms, stabile în v22: un semnal ține modelul,
form(model, schema)construiește un arbore de câmpuri cu reguli carequired(p.email), iar[formField]leagă fiecare input. Reactive Forms rămân alegerea pentru aplicații existente pline de ele sau pentru biblioteci care suportă doarControlValueAccessor.Apoi intră în detalii
- Semnalul-model este sursa de adevăr — fără copie internă, fără
patchValue:model.set(data)umple formularul, iar formularul scrie direct în model. - Starea fiecărui câmp e semnale:
f.email().invalid(),touched(),errors(); tot formularul e și el un câmp (f().valid(),f().submitting()). - Afișează erorile doar după
touched()și validează întotdeauna și pe server — validarea pe client e UX, nu securitate.
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" />Probabil te întreabă apoi
Cum ai valida că două câmpuri de parolă coincid?
Semnal de alarmă
Validează manual în handler-ul de submit cu
if-uri sau spune că Signal Forms sunt încă doar experimentale în v22.- Semnalul-model este sursa de adevăr — fără copie internă, fără
J-18Ce face
@defer? PerformanțăSpune asta întâi
@deferîncarcă lazy o parte dintr-un template: componentele din el ajung într-un chunk separat, descărcat doar când se declanșează un trigger — impliciton idle, sauon viewport,on interaction,when cond(). Reduce bundle-ul inițial fără să atingi rutele.Apoi intră în detalii
@placeholderse afișează înainte de trigger (necesar pentruon viewport/interaction/hover),@loadingîn timpul descărcării,@errordacă descărcarea eșuează.- Capcană: dacă o componentă din defer e referită și altundeva în același fișier (în afara blocului), e încărcată eager și chunk-ul se pierde.
prefetch on hoverdescarcă din timp și randează mai târziu — bun pentru acțiuni „la un click distanță”.
@defer (on viewport) { <app-comments [postId]="post().id" /> } @placeholder { <div class="comments-skeleton"></div> } @loading (after 100ms) { <app-spinner /> }Probabil te întreabă apoi
Care e diferența dintre
@deferși@if?Semnal de alarmă
Crede că
@defere doar un@ifîntârziat și nu pomenește de chunk-ul JavaScript separat.J-19Cum știe Angular când să actualizeze ecranul? Ce s-a schimbat în v21 și v22? Change detection
Spune asta întâi
Change detection-ul re-randează template-urile când ceva îl anunță pe Angular: se schimbă un semnal citit în template, are loc un eveniment din template, pipe-ul async emite sau se apelează
markForCheck(). Din v21 aplicațiile noi sunt zoneless (fără zone.js), iar din v22 componentele suntOnPushimplicit.Apoi intră în detalii
- Fără zone.js, un
setTimeoutsau unsubscribe()care schimbă un câmp simplu nu actualizează UI-ul. Soluția e un semnal (sautoSignal()/ pipe-ul async). - O componentă
OnPushe verificată doar când un input primește o referință nouă, are loc un eveniment în ea, se schimbă un semnal pe care îl citește sau e marcată pentru verificare — altfel subarborele ei e sărit. ChangeDetectionStrategy.Eagereste vechiul comportament „verifică mereu” (Defaulte deprecat);ng updateîl adaugă unde e nevoie.
Probabil te întreabă apoi
O valoare setată în
setTimeoutnu apare pe ecran. De ce și cum rezolvi?Semnal de alarmă
Spune că „zone.js detectează automat orice schimbare” într-o aplicație v21+ sau repară actualizările lipsă presărând
detectChanges().- Fără zone.js, un
J-20Cum testezi o componentă într-un proiect Angular nou? Testare
Spune asta întâi
Din v21
ng testrulează Vitest (Karma e deprecat). Creez componenta cuTestBed.createComponent(), setez input-urile, facawait fixture.whenStable()ca să se termine randarea zoneless, apoi verific înfixture.nativeElement.Apoi intră în detalii
- Testează comportamentul pe care îl vede utilizatorul (text, click-uri, output-uri emise), nu detaliile private de implementare.
- Input-urile se pot da ca semnale cu
inputBinding(); output-urile se urmăresc cuoutputBinding()șivi.fn(). - Pentru HTTP folosești
provideHttpClientTesting()șiHttpTestingControllerîn loc de un server real; serviciile și store-urile cu semnale se testează cuTestBed.inject(), fără 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); });Probabil te întreabă apoi
Cum ai testa o componentă care încarcă date prin HTTP?
Semnal de alarmă
Nu a scris niciodată un test sau se bazează pe
fakeAsync/tickdin zone.js într-un proiect zoneless cu Vitest.
Mid: feature-uri făcute singur
La acest nivel întrebările sunt despre alegeri: computed sau effect, ce operator RxJS, unde stă un provider, cum împarți o componentă, cum o testezi. Un răspuns bun de mid numește varianta, motivul și cazul în care cealaltă variantă câștigă.
M-01Când folosești
computed()și cândeffect()? De ce nu e bine să setezi semnale într-un effect? SignalsSpune asta întâi
computed()e pentru stare derivată: o valoare pură, lazy și memorată.effect()e pentru efecte secundare care duc valorile semnalelor spre lumea non-reactivă (DOM,localStorage, logging). Semnal → semnal printr-un effect e aproape mereu uncomputed()sau unlinkedSignal()deghizat.Apoi intră în detalii
- Dacă setezi un semnal într-un effect ai o rulare în plus, un moment în care ținta e învechită (un render cu valoare greșită) și un graf de dependențe greu de urmărit.
- Efectele nu sunt sincrone: setezi un semnal de 3 ori și efectul rulează o dată, cu valoarea finală. Efectele de componentă rulează în change detection; cele din servicii root, ca microtask-uri.
- Dependențele sunt dinamice: contează doar semnalele citite la ultima rulare. Folosești
untracked()pentru citiri care nu trebuie să redeclanșeze efectul. - Un effect are nevoie de context de injecție (constructor sau inițializare de câmp) ori de un
injectorexplicit; altfel primești NG0203. Pentru timere și listener-e foloseștionCleanup. - HTTP dintr-un effect e un semnal de alarmă: nu ai anulare, deci răspunsurile pot veni în altă ordine. Pentru asta există
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())); }Probabil te întreabă apoi
Ce faci dacă valoarea derivată trebuie să poată fi și editată de utilizator?
Semnal de alarmă
„Folosesc effect ca să țin două semnale sincronizate” sau „computed și effect sunt cam același lucru, doar că effect rulează mai des”.
M-02Ce problemă rezolvă
linkedSignal()? Dă un exemplu real. SignalsSpune asta întâi
E un semnal writable care se resetează dintr-o sursă: derivat ca un
computed(), dar îl poți șiset(). Cazuri tipice: o selecție care pornește de la prima opțiune dintr-o listă, numărul paginii care revine la 1 când se schimbă filtrele, o copie editabilă a unui input.Apoi intră în detalii
- Înlocuiește tiparul fragil semnal + effect: fără rulare în plus, fără render cu valoare veche.
- Forma lungă (
source+computation) îți dă valoareaprevious, deci poți păstra alegerea utilizatorului dacă încă există în lista nouă. - Un
set()manual e valabil doar până la următoarea schimbare a sursei; apoi câștigă computation. - Din v22.1, opțiunea
setinterceptează scrierile, de exemplu ca să limitezi o cantitate la stocul disponibil.
Probabil te întreabă apoi
Cum ai construi o ciornă editabilă a unui
input()cu un indicator „dirty”?Semnal de alarmă
Copierea input-ului într-un semnal în
ngOnInit(nu se mai resetează niciodată) sau sincronizarea cu un effect.M-03Cum funcționează
resource/httpResourceși de ce sunt mai bune decât un subscribe la HTTP într-un effect? HTTPSpune asta întâi
Un resource transformă încărcarea asincronă în semnale: o funcție reactivă
params(sau URL), iar când se schimbă, cererea veche e anulată și pornește una nouă. Primești gratisvalue(),status(),isLoading(),error()șireload(), fără race conditions.Apoi intră în detalii
resourceprimește unabortSignalîn loader;httpResourcetrece prinHttpClient, deci se aplică interceptorii;rxResourceprimește un stream Observable și se comportă caswitchMap.- Dacă params întoarce
undefined, statusul eidle: loader-ul nu rulează. În v22 poți și aruncaResourceParamsStatus.IDLE/LOADINGși poți înlănțui resurse cuctx.chain(). hasValue()e type guard;defaultValueeliminăundefined;parsevalidează sau mapează răspunsul (de ex. DTO → model, Zod).- Resursele sunt pentru citire. Scrierile (POST/PUT/DELETE) merg prin
HttpClient, apoi faciupdate()local pe resursă (optimist) saureload().
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> }Probabil te întreabă apoi
Cum ai șterge un element dintr-o listă încărcată cu
httpResourceși cum tratezi un eșec?Semnal de alarmă
Folosirea
httpResourcepentru un POST la click pe buton sau să nu știi ce se întâmplă cu o cerere în curs când se schimbă params.M-04Cum implementezi o căutare pe măsură ce scrii fără să trimiți o cerere la fiecare tastă? Signals
Spune asta întâi
În v22 înfășor semnalul de căutare cu
debounced(query, 300)și îi dauvalue()-ul unuihttpResource. Debounce-ul elimină zgomotul; resource-ul anulează cererile vechi, deci câștigă mereu ultima căutare.Apoi intră în detalii
debounced()întoarce unResource:value()se actualizează după ultima schimbare, iarisLoading()e true cât timp utilizatorul încă scrie.- E experimental în v22. Alternativa clasică e
toSignal(toObservable(query).pipe(debounceTime(300), distinctUntilChanged())): trei conversii pentru un singur debounce. waitpoate fi o funcție, de exemplu fără întârziere când câmpul e golit.- În Signal Forms există o regulă dedicată
debounce(path.field, 300), iarvalidateHttpare propria opțiunedebounce.
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: [] }, ); }Probabil te întreabă apoi
Debounce-ul singur nu previne răspunsurile venite în altă ordine. Ce le previne?
Semnal de alarmă
Un
setTimeoutîn handler-ul de input cusubscribe()manual și fără anularea cererii anterioare.M-05Cum combini semnale și RxJS într-o componentă fără să lași subscription-uri agățate? Când păstrezi RxJS? RxJS
Spune asta întâi
Semnale pentru starea afișată, RxJS la margini (evenimente, stream-uri, async complex).
toSignal()face subscribe și unsubscribe automat; la unsubscribe()manual puntakeUntilDestroyed(), care îl leagă deDestroyRef-ul componentei.Apoi intră în detalii
toSignal()fărăinitialValueare tipulT | undefined;requireSync: truemerge doar dacă sursa emite sincron (de ex. unBehaviorSubject).toObservable()emite asincron (printr-un effect) și doar ultima valoare dacă semnalul e setat de mai multe ori în același tick — nu e un stream de evenimente.takeUntilDestroyed()fără argument merge doar în context de injecție; în rest îi dai explicitinject(DestroyRef). Pentru cleanup fără RxJS foloseștiDestroyRef.onDestroy().- Păstrezi RxJS pentru websocket-uri, combinarea stream-urilor de evenimente, retry/backoff și politici de flattening (
switchMap,exhaustMap…). Semnalele nu au noțiunea de timp.
Probabil te întreabă apoi
De ce nu poți apela
toSignal()într-un handler de click?Semnal de alarmă
„Semnalele înlocuiesc complet RxJS” — sau invers: subscribe peste tot și copierea valorilor în câmpuri, fără unsubscribe.
M-06Explică
switchMapvsmergeMapvsconcatMapvsexhaustMap, cu un caz de utilizare pentru fiecare. RxJSSpune asta întâi
Diferă prin ce se întâmplă când vine o valoare nouă cât timp Observable-ul interior anterior încă rulează:
switchMapîl anulează (căutare),mergeMaprulează în paralel (upload-uri),concatMappune la coadă, în ordine (salvări ordonate),exhaustMapo ignoră pe cea nouă (submit, login).Apoi intră în detalii
- Alegerea operatorului e alegerea unei politici de concurență; cea greșită produce bug-uri reale: comenzi duble cu
mergeMappe butonul de plată, salvări pierdute cuswitchMapla autosave. mergeMapacceptă o limită de concurență, de ex.mergeMap(upload, 3).- Pui
catchErrorîn pipe-ul interior: după operatorul de flattening, prima eroare completează stream-ul exterior și căutarea „moare”. - Cu semnale,
httpResourceșirxResourceîți dau deja semanticaswitchMappentru citiri.
// 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 clicksProbabil te întreabă apoi
Pe care l-ai folosi pentru autosave-ul unui document și de ce nu
switchMap?Semnal de alarmă
„Folosesc mereu
switchMap” sausubscribe()imbricate în loc de un operator de flattening.- Alegerea operatorului e alegerea unei politici de concurență; cea greșită produce bug-uri reale: comenzi duble cu
M-07Într-o aplicație Angular 22 zoneless, ce declanșează change detection și de ce un câmp actualizat în
setTimeouts-ar putea să nu apară pe ecran? Change detectionSpune asta întâi
Fără zone.js nu mai e nimeni care să intercepteze timerele sau promise-urile. Change detection e programat când se schimbă un semnal citit în template, la un eveniment din template/host, de pipe-ul
async, demarkForCheck(),setInput()sau de o navigare. Un câmp simplu setat însetTimeoutsausubscribe()nu anunță pe nimeni — fă-l semnal.Apoi intră în detalii
- Din v22 componentele fără
changeDetectionsunt OnPush; comportamentul vechi eChangeDetectionStrategy.Eager(Defaulte deprecat).ng updateadaugă Eager unde e nevoie. - O componentă OnPush e verificată doar la o referință nouă pe input (
===), la un eveniment în ea sau în copii, la un semnal citit în template, la pipe-ulasyncsau lamarkForCheck(). Altfel tot subarborele e sărit. - Mutarea unui array primit ca input (
push) nu schimbă referința: folosești update-uri imutabile. markForCheck()e soluția de urgență pentru callback-uri vechi;detectChanges()e sincron și rar necesar.
// ✗ 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)); } }Probabil te întreabă apoi
Cum ai migra pas cu pas o componentă veche care se bazează pe Zone.js la OnPush + zoneless?
Semnal de alarmă
Apelarea
detectChanges()peste tot sau convingerea că OnPush înseamnă „componenta nu se actualizează decât când se schimbă input-urile”.- Din v22 componentele fără
M-08Prin ce diferă
viewChild()șicontentChildren()de vechii decoratori și când le poți citi în siguranță? ComponenteSpune asta întâi
Query-urile semnal întorc semnale, deci rezultatele sunt reactive: derivi din ele cu
computed()în loc dengAfterViewInitșiQueryList.changes. În constructor view-ul nu există încă, deci lucrul cu DOM-ul merge înafterNextRender();viewChild.required()aruncă eroare dacă e citit prea devreme.Apoi intră în detalii
viewChildcaută în template-ul propriu al componentei;contentChildcaută în ce a proiectat părintele (de ex. un container Tabs care își descoperă copiii Tab).contentChildrense uită implicit doar la copiii direcți; pentru potriviri mai adânci foloseștidescendants: true.readalege ce primești:ElementRef,TemplateRef,ViewContainerRef, o directivă sau (v22.2)Injector-ul copilului.- Capcană: un query după tipul unei componente în clasă o face eager, deci nu mai e separată într-un chunk de un bloc
@defer.
Probabil te întreabă apoi
Cum ar randa o componentă Tabs doar conținutul tab-ului activ?
Semnal de alarmă
Citirea unui view query în constructor sau în
ngOnInitși surpriza că eundefined.M-09Content projection cu
ng-contentvs unng-templaterandat cungTemplateOutlet: când alegi una și când cealaltă? Template-uriSpune asta întâi
ng-contente pentru sloturi statice: părintele creează conținutul o dată, fie că îl afișează copilul, fie că nu. Unng-templatee o rețetă pe care copilul o instanțiază când și de câte ori vrea, cu un context — necesar pentru conținut lazy și pentru API-uri de tip „cum arată un rând”.Apoi intră în detalii
- Dacă pui
ng-contentîntr-un@if, ascunzi DOM-ul, dar componentele proiectate există deja și ciclul lor de viață a rulat deja. - Sloturile numite folosesc
select(tag, atribut, clasă);ngProjectAsproiectează mai multe elemente într-un slot fără wrapper. - Contextul trimite date înapoi:
$implicitajunge înlet-item. Fără o directivă cungTemplateContextGuard, variabilelelet-suntany. - Un template vede injectorul locului unde a fost declarat; din v21.2
[ngTemplateOutletInjector]="'outlet'"îl face să folosească injectorul locului unde e randat.
@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>Probabil te întreabă apoi
Cum faci variabilele
let-ale unui template să fie tipizate?Semnal de alarmă
Convingerea că un
@ifîn jurul luing-contentface conținutul proiectat lazy.- Dacă pui
M-10Cum ai construi o componentă de tabel reutilizabilă și type-safe fără să devină o „god component”? Componente
Spune asta întâi
Faci componenta generică (
DataTable<T>):Te dedus dinrows, cheile coloanelor sunt limitate lakeyof T, output-urile sunt tipizate. Personalizarea se face prin template-uri de celulă, nu prin input-uri boolean, iar wrapper-ele de domeniu stau în feature.Apoi intră în detalii
- Semnul unei god component: date
any[], flag-uriisProductTable, 30 de input-uri și cunoștințe de domeniu înshared/. - Template-urile de celulă sunt găsite cu
contentChildren()pe o directivă;ngTemplateContextGuardtipizeazălet-rowcaTîn loc deany. - Comportamentul îl dai ca funcții (
trackBy,displayWith,compareWith), ca să nu trebuiască să știe componenta forma datelor. - O greșeală de tipar în cheia unei coloane devine eroare de compilare, nu o coloană goală în producție.
Probabil te întreabă apoi
Unde ar sta tabelul specific pentru utilizatori și ce ar adăuga peste cel generic?
Semnal de alarmă
Adăugarea unui nou input boolean de fiecare dată când o pagină are nevoie de ceva diferit.
- Semnul unei god component: date
M-11Cum randezi o componentă aleasă la runtime, de exemplu widget-urile unui dashboard dintr-o configurație? Componente
Spune asta întâi
Urci o scară: un set mic și cunoscut →
@switch; componenta e o valoare →NgComponentOutletcuinputs; control complet (output-uri, directive) →ViewContainerRef.createComponent()cubindings(inputBinding,outputBinding,twoWayBinding).Apoi intră în detalii
- Un registru de loader-e
() => import(…)face din fiecare widget un chunk lazy separat. inputBinding('title', () => …)e reactiv, spre deosebire de vechiulsetInput()pe care trebuia să-l apelezi din nou la fiecare schimbare, plus subscription-uri manuale la output-uri.ComponentFactoryResolvera fost eliminat în v22: dai clasa direct luicreateComponent.- Pentru toast-uri și dialoguri în afara arborelui folosești funcția
createComponent()cu unEnvironmentInjector, apoiappRef.attachView()ca să intre în change detection, și o distrugi tu.
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)), ], }); }Probabil te întreabă apoi
Cum te asiguri că toate widget-urile acceptă aceleași input-uri și output-uri?
Semnal de alarmă
Folosirea
ComponentFactoryResolversau ainnerHTMLcu tag-uri de componente.- Un registru de loader-e
M-12Cum gestionezi starea unei pagini de feature fără o librărie de state? Unde intră componentele smart și dumb? Arhitectură
Spune asta întâi
Un serviciu store bazat pe semnale: semnale writable private, semnale publice read-only (
asReadonly()) și selectoricomputed(), plus metode cu nume de domeniu care fac update-uri imutabile. Pagina rutată (smart) furnizează și injectează store-ul; componentele dumb primesc date prin input-uri și emit intenții prin output-uri.Apoi intră în detalii
- Alegi scope-ul după durata de viață: starea de feature în
providerspe pagină sau rută (moare odată cu ea), starea globală (user, coș, temă) într-un@Service()root. - Datele de pe server stau într-un
httpResourceîn store; starea care trebuie să supraviețuiască unui refresh sau să poată fi trimisă ca link stă în URL (query params +input()). - O componentă dumb poate avea stare UI locală și poate injecta lucruri de UI (
ElementRef,DestroyRef), dar niciodată servicii de date sau router-ul — altfel nu mai poate fi reutilizată sau testată ușor, iar o listă de 50 de carduri face 50 de cereri. - NgRx SignalStore merită când echipa vrea convenții, plugin-uri și devtools, nu din oficiu.
Probabil te întreabă apoi
De ce expui semnale read-only în loc de cele writable?
Semnal de alarmă
Toată starea într-un singur serviciu global root sau semnale writable expuse pe care orice componentă le modifică direct.
- Alegi scope-ul după durata de viață: starea de feature în
M-13Explică ierarhia DI: când furnizezi în root, pe o rută sau pe o componentă și când îți trebuie un
InjectionToken? DISpune asta întâi
inject()caută în injectorii de element (elementul curent, apoi părinții din DOM), apoi în injectorul rutei, în root și în platform, și aruncă NG0201 dacă nu găsește nimic. Root (@Service()) înseamnă un singleton pe toată aplicația;providerspe rută, o instanță per feature;providerspe componentă, o instanță per componentă și copiii ei. Token-urile sunt pentru valori care nu sunt clase: config, URL-uri, funcții.Apoi intră în detalii
@Service()(v22) e echivalent cuprovidedIn: 'root', e tree-shakable și permite doarinject().autoProvided: falsete obligă să-l pui înproviders.inject()e mai bun decât DI prin constructor: fără lanțurisuper(…), merge în guards, interceptori și factory-uri și permite funcții reutilizabileinjectX(). Are nevoie de context de injecție (altfel NG0203).useClass,useValue,useFactory,useExistingșimulti: truepentru array-uri de tip plugin;optional,self,skipSelf,hostmodifică căutarea.viewProvidersnu sunt vizibili pentru conținutul proiectat;EnvironmentProviders(provideX()) pot sta doar la nivel de aplicație sau rută, nu într-o componentă.
Probabil te întreabă apoi
Ce se întâmplă cu un serviciu furnizat pe o rută lazy când utilizatorul pleacă de pe ea?
Semnal de alarmă
Adăugarea unui serviciu root în
providerspe o componentă „ca să fie” — asta creează o a doua instanță și bug-uri de stare greu de găsit.M-14Guards și resolvers funcționale: care e diferența dintre
canMatchșicanActivateși când ai evita un resolver? RoutingSpune asta întâi
Guard-urile sunt funcții simple care folosesc
inject().canMatchdecide dacă ruta „există” pentru utilizator — dacă nu, router-ul încearcă ruta următoare și chunk-ul lazy nu se descarcă.canActivateblochează intrarea pe o rută care s-a potrivit. Un resolver întârzie navigarea până vin datele; pentru liste și dashboard-uri preferhttpResourceîn pagină.Apoi intră în detalii
- Întorci
true/false, unUrlTreesau unRedirectCommand(redirect + opțiuni de navigare, careplaceUrl); din v22.2 poți și arunca unRedirectCommand. - Resolver: pagina apare cu datele gata, dar navigarea așteaptă, iar o eroare o anulează.
httpResource: navigare instantă, loading-ul și eroarea sunt stare în UI. - Cu
withComponentInputBinding(), datele rezolvate, params și query params ajung cainput()-uri. - Guard-urile sunt UX, nu securitate: și API-ul trebuie să verifice autorizarea.
canDeactivateprotejează modificările nesalvate.
Probabil te întreabă apoi
Cum ai afișa un dashboard diferit pentru admini și pentru utilizatori obișnuiți pe aceeași cale?
Semnal de alarmă
Guard-uri clasă cu injecție prin constructor ca variantă implicită sau tratarea unui guard ca singurul control de acces.
- Întorci
M-15Scrie un interceptor HTTP funcțional care adaugă token-ul de autentificare. Care sunt capcanele? HTTP
Spune asta întâi
Un
HttpInterceptorFne o funcție(req, next)care întoarcenext(req)— sau o clonă a cererii, pentru că cererile sunt imutabile. Se înregistrează cuprovideHttpClient(withInterceptors([…]))și poate folosiinject().Apoi intră în detalii
- Atașezi token-ul doar pentru API-ul tău (verifici URL-ul), niciodată pentru host-uri terțe.
- Ordinea contează: cererile trec prin interceptori în ordinea din listă, răspunsurile se întorc în ordine inversă.
- Apelezi
inject()la începutul funcției, nu în callback-uri RxJS precumcatchError, care rulează mai târziu, în afara contextului de injecție. - Folosești
HttpContextTokenpentru metadate per cerere (de ex. să sari peste bara de loading) în loc de header-e false. ȘihttpResource, șivalidateHttptrec prin interceptori.
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]));Probabil te întreabă apoi
Cum ai trata un 401 prin reîmprospătarea token-ului o singură dată și reluarea cererii originale?
Semnal de alarmă
Modificarea directă a
req.headerssau trimiterea token-ului către orice URL.M-16Cum structurezi tratarea erorilor într-o aplicație Angular: erori HTTP, erori de randare și restul? Arhitectură
Spune asta întâi
Pe straturi: erorile HTTP previzibile (404, 422) sunt stare de UI în store sau componentă (
resource.error()); cele transversale (401, 5xx) merg într-un interceptor; o zonă stricată a paginii e izolată cu@boundary; tot ce nu e prins ajunge într-unErrorHandlerglobal custom, pentru logging și telemetrie.Apoi intră în detalii
@boundary(v22.2, developer preview) are blocuri@error (let e = $error; let reset = $reset), opțional cu condițiiwhen— câștigă prima potrivire.@boundaryprinde doar erori de randare, nu erori din handler-e de evenimente sau din cod async — acelea ajung tot laErrorHandler.provideBrowserGlobalErrorListeners()trimitewindow.onerrorși promise-urile respinse netratate cătreErrorHandler.- Un interceptor ar trebui să arunce eroarea mai departe (
throwError(() => err)) după reacția transversală, ca apelantul să poată reacționa și el. Erorile de validare nu sunt excepții: stau înerrors()din formular.
@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()" /> }Probabil te întreabă apoi
Unde ajunge o eroare aruncată într-un handler de click?
Semnal de alarmă
Un
try/catchcuconsole.logîn fiecare componentă sau înghițirea erorilor în interceptor, ca nimeni altcineva să nu le mai vadă.M-17Cum funcționează validarea în Signal Forms, inclusiv cea asincronă, și prin ce diferă de Reactive Forms tipizate? Formulare
Spune asta întâi
În Signal Forms modelul e un semnal simplu, iar
form(model, schema)construiește peste el un arbore de câmpuri, cu reguli declarate într-o funcție schemă (required,email,validate,validateHttp…). Orice stare de câmp e un semnal:valid(),errors(),pending(),touched(). Reactive Forms țin o copie proprie a valorii înFormControl-uri și expun schimbările ca Observables.Apoi intră în detalii
- Fără copie internă:
model.set()actualizează input-urile, iar tastarea actualizează modelul — fărăpatchValue/valueChanges. - Regulile între câmpuri folosesc
valueOf()învalidate; cele condiționale,when,hidden,disabled(câmpurile hidden și disabled nu se validează); regulile reutilizabile,schema()+apply(). validateHttptrece prinHttpClient(deci se aplică interceptorii) și are opțiuneadebounceîn v22;validateAsyncmerge cu orice resource.pending()e true cât timp rulează.- Erorile de la server din
submission.actionpot fi întoarse cu unfieldTree, ca să ajungă pe câmpul potrivit. - Migrarea poate fi treptată cu
SignalFormControlșicompatForm; Reactive Forms tipizate (nonNullable,getRawValue()) rămân valide pentru codul existent.
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' }), }); });Probabil te întreabă apoi
Cum ai valida că data de sfârșit e după data de început?
Semnal de alarmă
Un validator async care trimite o cerere la fiecare tastă, fără debounce, sau să nu știi că între timp câmpul e
pending.- Fără copie internă:
M-18Cum construiești un form control custom (de ex. un rating cu stele) care merge cu Signal Forms? Dar cu Reactive Forms? Formulare
Spune asta întâi
Pentru Signal Forms implementezi
FormValueControl<T>: singurul membru obligatoriu evalue = model(); apoi[formField]merge pe el exact ca pe uninput. Pentru Reactive Forms, varianta clasică eControlValueAccessor, înregistrat cuNG_VALUE_ACCESSOR.Apoi intră în detalii
- Input-urile opționale ca
disabled,invalid,touchedsunt completate automat de formular — le declari doar pe cele pe care le folosești. În v22, un outputtouchanunță că utilizatorul a atins controlul. - Controalele boolean implementează
FormCheckboxControl, cucheckedîn loc devalue. - Un CVA are nevoie de patru metode (
writeValue,registerOnChange,registerOnTouched,setDisabledState) și trebuie să apelezi tuonChange; dacă uiționTouched, nu mai merge „arată erorile după touch”. - Interoperabilitate în v22: Signal Forms poate lega CVA-uri, iar formularele template/reactive suportă
FormValueControl, deci un singur control le poate servi pe amândouă în timpul migrării.
Probabil te întreabă apoi
Cum îți dezactivează formularul controlul și trebuie să tratezi tu asta?
Semnal de alarmă
Trimiterea unui
FormControlca input componentei copil și modificarea lui acolo.- Input-urile opționale ca
M-19Cum funcționează
@defer, ce trigger-e ai folosi și ce face ca un bloc deferred să nu fie de fapt lazy? PerformanțăSpune asta întâi
@defermută componentele din bloc într-un chunk separat, încărcat la un trigger:on idle(implicit),viewport,interaction,hover,timer,immediatesauwheno condiție.prefetchdescarcă mai devreme decât randează. Rămâne lazy doar dacă dependențele sunt standalone și nu sunt folosite altundeva în fișier.Apoi intră în detalii
- Dacă referi o componentă deferred în clasă (de ex.
viewChild(HeavyChart)) sau altundeva în template, devine eager. @placeholderapare înainte de trigger (și e elementul observat pentruviewport/interaction/hoverfără țintă);@loading (after 100ms; minimum 500ms)evită pâlpâirea;@errortratează o descărcare de chunk eșuată.whense declanșează o singură dată; mai multe trigger-e se combină cu SAU.- Noutăți:
on viewport({ rootMargin: '200px' })(v21),on idle(500)(v22). Cu SSR, trigger-elehydrate on …dau hidratare incrementală, activă implicit în 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> }Probabil te întreabă apoi
Cum ai verifica că blocul chiar a ajuns într-un chunk separat?
Semnal de alarmă
Punerea conținutului vizibil imediat (above the fold) în
@defersau convingerea că@deferamână o cerere HTTP, nu încărcarea de cod.- Dacă referi o componentă deferred în clasă (de ex.
M-20Cum testezi o componentă cu input-uri semnal și un serviciu care face HTTP, folosind Vitest în Angular 21+? Testare
Spune asta întâi
Componente:
TestBed.createComponent(Cmp, { bindings: [inputBinding('value', signal)] }), apoiawait fixture.whenStable(), pentru că nu mai există zone. HTTP:provideHttpClient()+provideHttpClientTesting(), apoiHttpTestingController.expectOne(url).flush(data)șiverify()înafterEach.Apoi intră în detalii
- Vitest e runner-ul implicit din v21 (
@angular/build:unit-test); Karma e deprecat. Foloseștivi.fn()pentru spioni pe output-uri șivi.useFakeTimers()în loc defakeAsync. - Schimbarea semnalului legat actualizează input-ul ca într-un părinte real — nu mai ai nevoie de
setInput(). - Store-urile cu semnale sunt cel mai ușor de testat:
TestBed.inject(), apelezi metode, verificicomputed()-urile. Efectele rulează asincron, deci le forțezi cuTestBed.tick(). - Testezi comportamentul prin DOM sau prin harness-uri, nu câmpuri private.
TestBed.createDirective()(v22.2) testează o directivă fără componentă-gazdă scrisă de mână.
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 }); });Probabil te întreabă apoi
Cum ai testa o componentă care folosește
httpResource?Semnal de alarmă
Apeluri către backend-ul real în testele unitare sau
detectChanges()apelat de mai multe ori și verificări pe câmpuri private.- Vitest e runner-ul implicit din v21 (
Senior: mecanisme interne, arhitectură, compromisuri
Interviurile de senior verifică dacă poți decide pentru o echipă: cum merge framework-ul pe dedesubt, cum rămâne o aplicație ușor de întreținut cu 50 de developeri, cât costă SSR, securitatea și performanța. Răspunde mereu cu o poziție, compromisul și cum ai introduce-o într-un proiect real.
S-01Cum funcționează semnalele pe dinăuntru? Explică graful producător/consumator, push-pull și de ce sunt glitch-free. Signals
Spune asta întâi
Semnalele formează un graf reactiv construit din citiri:
signal()e producător,computed()e ambele, iar effect-urile și template-urile sunt consumatori live. O scriere doar crește o versiune și împinge o notificare „poate e murdar”; valorile sunt trase și recalculate leneș când cineva citește, deci uncomputedvede mereu input-uri consistente și rulează o singură dată pe un set de scrieri.Apoi intră în detalii
- Dependențele sunt dinamice: la fiecare rulare lista de producători se reconstruiește din ce s-a citit efectiv, deci un semnal citit doar într-o ramură
ifinactivă nu declanșează nimic. - Egalitatea oprește propagarea: versiunea crește doar dacă valoarea nouă nu e egală (implicit
Object.is). Uncomputedal cărui rezultat nu se schimbă nu-și trezește consumatorii;{ equal }personalizează asta pentru valori recreate de la server. - Consecință: mutația (
items().push()) nu anunță pe nimeni, pentru că referința și versiunea rămân aceleași. Imutabilitatea nu e stil, e felul în care graful detectează schimbarea. - Effect-urile sunt asincrone și programate odată cu change detection (view effects înainte de verificarea componentei lor, root effects înaintea tuturor componentelor). Semnalele nu sunt un mecanism de evenimente și nu ordonează în timp efectele secundare — acolo e teritoriul 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]), });Probabil te întreabă apoi
De ce un
effect()care scrie în alt semnal e de obicei unealta greșită și ce folosești în loc?Semnal de alarmă
„Un semnal e doar un BehaviorSubject cu API mai drăguț” sau „
set()re-randează componenta sincron” — arată că nu înțelegi lenea, versiunile și push-pull.- Dependențele sunt dinamice: la fiecare rulare lista de producători se reconstruiește din ce s-a citit efectiv, deci un semnal citit doar într-o ramură
S-02Într-o aplicație zoneless cu componente OnPush, ce programează de fapt change detection și ce sare Angular? Change detection
Spune asta întâi
Fără zone.js nimeni nu mai interceptează
setTimeoutsau promise-urile, deci Angular randează doar când i se spune: se schimbă un semnal citit în template, se declanșează un listener din template/host, pipe-ulasyncemite,markForCheck(),setInput()sau o navigare. Un subarbore OnPush e sărit dacă nu-l marchează unul dintre acestea, deci munca e proporțională cu ce s-a schimbat, nu cu mărimea arborelui.Apoi intră în detalii
- Template-ul e un consumator reactiv: când se schimbă un semnal pe care l-a citit, Angular marchează acel view pentru refresh (ca
markForCheck), iar change detection ajunge doar la componentele afectate. - OnPush compară input-urile prin referință (
===): dacă muți un obiect primit printr-un input, copilul nu se actualizează — aceeași regulă de imutabilitate ca la semnale. - Bug-ul clasic: un câmp simplu setat în
setTimeout,subscribe()sau un callback de bibliotecă externă (WebSocket, Maps) — „se actualizează doar după ce dau click undeva”. Soluția e un semnal sautoSignal();markForCheck()e ieșirea de urgență pentru cod vechi. detectChanges()e sincron și rareori necesar (măsurători, teste);markForCheck()e varianta asincronă recomandată.
// ✗ 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)); } }Probabil te întreabă apoi
Un copil OnPush primește un obiect printr-un input, iar părintele îl modifică pe loc. Ce se întâmplă și cum rezolvi la nivel de echipă?
Semnal de alarmă
Să presari
detectChanges()peste tot până se actualizează ecranul, sau să crezi că zoneless înseamnă „Angular verifică mai rar, deci e mai buggy”.- Template-ul e un consumator reactiv: când se schimbă un semnal pe care l-a citit, Angular marchează acel view pentru refresh (ca
S-03Cum ai migra o aplicație mare, bazată pe zone.js, la zoneless fără să oprești dezvoltarea de feature-uri? Change detection
Spune asta întâi
Incremental: întâi faci componentele corecte sub OnPush, cu starea în semnale, cât timp zone.js e încă plasa de siguranță, apoi scoți zone.js când nimic nu mai depinde de el. Riscul nu e Angular, ci codul și bibliotecile care se bazau tacit pe zone.js ca să declanșeze randări.
Apoi intră în detalii
- Întâi inventarul: câmpuri simple setate în callback-uri asincrone,
subscribe()în câmpuri, biblioteci care așteaptăNgZone.onStable. Angular 21 aduce un instrument de analiză (onpush_zoneless_migration, prin serverul MCP al CLI-ului,ng mcp). - Migrezi întâi componentele frunză, feature cu feature: scoți
Eager, muți starea în semnale /toSignal(), review și teste. Fiecare PR e mic și reversibil. - Până la final păstrezi
provideZoneChangeDetection({ eventCoalescing: true })și polyfill-ul zone.js; apoi le scoți pe amândouă și urmărești bug-urile de tip „se actualizează doar după un click”. - Se schimbă și testele:
fakeAsyncare nevoie de zone.js, deci treci laawait fixture.whenStable()și fake timers din Vitest. - În echipă: o regulă de lint/review ca tot codul nou să folosească semnale pentru starea din template și un checklist scurt în template-ul de PR, ca zona migrată să nu regreseze.
Probabil te întreabă apoi
O bibliotecă de grafice actualizează datele în propriile callback-uri. Cum o integrezi fără zone.js?
Semnal de alarmă
„Scoatem zone.js într-un singur PR mare și reparăm ce se strică în QA” — fără inventar, fără plasă de siguranță, fără plan pentru biblioteci și teste.
- Întâi inventarul: câmpuri simple setate în callback-uri asincrone,
S-04Cum proiectezi stratul de stare al unei aplicații mari: serviciu cu semnale, NgRx SignalStore sau NgRx Store clasic? Arhitectură
Spune asta întâi
Prima întrebare nu e „ce bibliotecă”, ci unde trăiește fiecare bucată de stare și cine o deține. Un serviciu cu semnale private, selectori
computed()și metode cu nume clare acoperă majoritatea aplicațiilor; SignalStore când vrei o formă impusă, entități și features reutilizabile între multe echipe; NgRx Store clasic doar când chiar ai nevoie de jurnal de acțiuni, time-travel sau fluxuri bazate pe evenimente.Apoi intră în detalii
- Ții starea la cel mai jos nivel care are nevoie de ea (componentă → feature → global) și o ridici doar când apare un consumator nou. Starea unei pagini e furnizată pe rută sau pe componentă, nu în root, ca să nu „rămână lipită” între vizite.
- Datele de la server sunt un cache, nu starea ta: le ții în
httpResource/resourceși reîncarci după scrieri. Și URL-ul e stare (filtre, paginare). - Reguli care scalează: expui semnale read-only (
asReadonly()), o singură sursă de adevăr cu restul derivat, fără store-uri care se injectează circular — de obicei semn că lipsește un facade sau că un concept e pus greșit. - Cu SignalStore, doar metodele apelează
patchState,rxMethodse ocupă de fluxurile asincrone (cucatchErrorîn stream-ul interior), iarsignalStoreFeatureelimină codul de loading/error copiat. Bibliotecile pot coexista cu un NgRx Store existent.
Probabil te întreabă apoi
Două feature-uri au nevoie de aceleași date despre coș. Unde stă store-ul și cum comunică fără să se importe reciproc?
Semnal de alarmă
„Mereu NgRx pentru orice lucru serios” sau „un singur store global pentru toată aplicația” — unealta aleasă înainte să înțelegi cine deține starea și cât trăiește.
S-05Cum structurezi un codebase mare în cod de feature și cod shared și cum impui granițele într-un monorepo? Arhitectură
Spune asta întâi
Organizezi pe domenii, nu pe tipuri de fișiere: feature-urile depind de core și shared, shared nu depinde de nimic din domeniu, iar feature-urile nu se importă direct între ele. Regulile care trăiesc doar într-un wiki se încalcă în prima săptămână, așa că le pun în lint —
no-restricted-importsîntr-o singură aplicație, sau tag-uri Nx cu@nx/enforce-module-boundariesîntr-un monorepo.Apoi intră în detalii
- Tag-uri pe două dimensiuni: scope (cine cu cine vorbește:
scope:shop,scope:shared) × type (direcția dependențelor: feature → ui / data-access → util). Constrângerile din ambele se aplică cu AND. - Un singur API public per feature/bibliotecă (un barrel doar la graniță, niciodată în fiecare folder); importurile adânci și importurile statice ale bibliotecilor lazy sunt încălcări.
- Granularitatea e un compromis: biblioteci mici pe domeniu și tip dau grafuri
nx affectedînguste și cache hit-uri; o bibliotecă shared uriașă face ca fiecare PR să afecteze tot repo-ul. - Muți în shared doar ce e generic, stabil și deja folosit în mai multe feature-uri („regula lui trei”); componentele de domeniu folosite și în alte părți rămân în feature-ul care le deține și sunt exportate prin API-ul lui public.
'@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'] }, ], }],Probabil te întreabă apoi
Un developer „repară” o eroare de lint cu
sourceTag: '*'. Ce faci și cum previi să se repete?Semnal de alarmă
Foldere pe tipuri (
components/,services/) și unsharedpe post de sertar cu de toate, cu granițe „stabilite verbal”.- Tag-uri pe două dimensiuni: scope (cine cu cine vorbește:
S-06Cum ai aplica ports & adapters în Angular? Unde stă HTTP-ul și când nu merită? Arhitectură
Spune asta întâi
Domeniul e TypeScript simplu, use case-urile depind de porturi scrise ca clase abstracte (care sunt și token-uri DI), iar HTTP-ul stă doar în adaptoare, la margine, care mapează DTO-urile în entități. Singurul loc care leagă un port de un adaptor e composition root-ul —
app.config.tssau providerii unei rute. Pentru ecrane CRUD simple e prea mult.Apoi intră în detalii
- Clasă abstractă în loc de interfață: o interfață dispare la compilare, deci ai avea nevoie de un
InjectionTokenseparat; clasa abstractă e și tipul, și token-ul. - Erorile fac parte din contract: adaptorul traduce timeout-urile/503 într-o eroare de domeniu, deci nicio componentă nu vede vreodată
HttpErrorResponse. - Câștigul se vede la testare: regulile de domeniu ca funcții pure, use case-urile cu un adaptor in-memory și o singură suită de teste de contract rulată pe fiecare adaptor.
- Varianta pragmatică: un domeniu pur pentru regulile care contează, un port doar unde ai (sau vei avea) mai multe implementări, restul în structura obișnuită data-access/ui — iar straturile le impui prin 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 }]Probabil te întreabă apoi
De ce e o idee proastă
@Injectable({ providedIn: 'root', useClass: HttpTaskRepository })pe port?Semnal de alarmă
Componente care injectează direct
HttpClientsau adaptorul concret, DTO-uri care ajung în template-uri, sau patru straturi pentru două formulare „pentru că Clean Architecture”.- Clasă abstractă în loc de interfață: o interfață dispare la compilare, deci ai avea nevoie de un
S-07Când ai alege micro-frontend-uri cu Native Federation și când te-ai opune? Arhitectură
Spune asta întâi
Micro-frontend-urile rezolvă o problemă organizațională — echipe autonome care trebuie să livreze independent și azi se blochează reciproc la release — nu una tehnică. Dacă nu pot numi două echipe care se blochează, recomand o singură aplicație într-un monorepo, cu rute lazy și granițe impuse; MFE înrăutățește performanța și doar mută codul încâlcit între repository-uri.
Apoi intră în detalii
- Motive valide: multe echipe cu ritmuri diferite, organizații foarte mari, o migrare strangler fig dintr-o aplicație veche, produse separate sub aceeași experiență.
- Angular trebuie să fie singleton cu
strictVersion, ideal exact aceeași versiune peste tot: două runtime-uri înseamnă componente care nu pot intra în arborele shell-ului și servicii cu două instanțe. Un remote pe alt major se izolează ca Web Component sau iframe. - Tratezi exposes-urile fiecărui remote ca API public cu semver; comunici prin URL, apoi evenimente tipizate, apoi câteva servicii singleton partajate — niciodată un store global comun (asta e un monolit distribuit).
- Planifici costurile din prima zi: timeout și fallback per remote, un manifest per mediu, numele și versiunea remote-ului în fiecare eroare logată, e2e pe combinația din producție.
Probabil te întreabă apoi
Un remote nu se încarcă în producție. Cum trebuie să se comporte shell-ul, pentru o rută întreagă față de un widget dintr-o pagină?
Semnal de alarmă
„Avem nevoie de micro-frontend-uri pentru scalabilitate și performanță” sau setarea
strictVersion: falseca să dispară avertismentele de versiune.S-08Când e Angular Elements unealta potrivită și care sunt capcanele când livrezi o componentă Angular ca element custom? Componente
Spune asta întâi
Când o componentă trebuie să trăiască în afara unei aplicații Angular: widget-uri pentru CMS, site-uri de clienți, un design system pentru alte framework-uri sau un remote care nu poate împărți versiunea de Angular.
createCustomElement()transformă input-urile în atribute/proprietăți și output-urile înCustomEvent-uri, dar fiecare bundle își aduce propriul runtime (≈ 33 kB transferați) și nu există DI comun — în aceeași aplicație Angular e aproape mereu alegerea greșită.Apoi intră în detalii
- Atributele sunt mereu string-uri: input-urile numerice/booleene au nevoie de
numberAttribute/booleanAttribute, iar obiectele/array-urile merg doar prin proprietăți. - Evenimentele nu fac bubble și nu sunt composed: asculți direct pe element. Folosești nume specifice pentru output-uri (
ratingChange, nuchange). - Folosești
ViewEncapsulation.ShadowDomca CSS-ul paginii-gazdă să nu intre; tematizezi prin variabile CSS și::part(). - Grupezi widget-urile unei echipe într-un singur bundle (un
createApplication(), mai multe apeluridefine()), folosești un tag diferit de selectorul componentei și nu refolosești un element distrus — creezi altul.
Probabil te întreabă apoi
Trei echipe livrează fiecare câte un widget construit separat pe aceeași pagină. Ce costă asta și ce ai schimba?
Semnal de alarmă
Să folosești Angular Elements ca să refolosești componente între ecranele aceleiași aplicații Angular, sau pentru conținut critic pentru SEO care ar trebui randat pe server.
- Atributele sunt mereu string-uri: input-urile numerice/booleene au nevoie de
S-09Cum decizi între CSR, SSR și prerender într-o aplicație Angular și ce costă operarea? SSR
Spune asta întâi
Pe rută, nu pe proiect:
app.routes.server.tsstabileșteRenderMode.Prerender,ServersauClientpentru fiecare cale. Prerender pentru paginile publice stabile, randare pe server pentru paginile publice cu date proaspete, iar zona din spatele login-ului rămâne pe client. SSR îmbunătățește LCP și SEO; nu micșorează JavaScript-ul și nu repară o aplicație lentă după hidratare.Apoi intră în detalii
- Prerender cu parametri cere
getPrerenderParams;PrerenderFallback.Server(implicit) îți permite să prerandezi top N produse, iar restul la cerere. Fiecare URL e o randare completă la build. - Modul Server înseamnă un proces Node: TTFB e cel puțin cât cel mai lent API apelat la randare, CPU per cerere și stare la nivel de modul partajată între utilizatori.
- Codurile de status corecte contează pentru SEO: setezi 404 prin
RESPONSE_INITîn loc de un soft 404; cache-ul e pe rută —public, s-maxagedoar pentru conținut identic,private, no-storepentru HTML personal. - În v22 configurezi
allowedHosts(sauNG_ALLOWED_HOSTS): un host necunoscut primește 400, ca protecție SSRF. Dacă nimeni nu poate opera un server Node, folosești Prerender cuoutputMode: 'static'.
Probabil te întreabă apoi
Paginile SSR au devenit lente după ce un API din backend s-a degradat. De ce și ce schimbi?
Semnal de alarmă
„Activăm SSR peste tot, face aplicația mai rapidă” — ignorând că TTI aproape nu se schimbă, costul serverului și paginile personale cache-uite public pe CDN.
- Prerender cu parametri cere
S-10Explică hidratarea incrementală și event replay în v22. Cum le folosești pe o pagină de produs? SSR
Spune asta întâi
În v22 un simplu
provideClientHydration()activează hidratarea incrementală și, odată cu ea, event replay. Blocurile@defer (hydrate on …)sunt randate pe server, dar rămân dehidratate în browser — JS-ul lor nici nu e descărcat — până se declanșează trigger-ul; click-urile făcute înainte sunt puse în coadă și redate, iar un click într-un bloc dehidratat îl hidratează.Apoi intră în detalii
- Spre deosebire de un
@deferobișnuit, conținutul final e deja pe ecran, deci poți amâna și deasupra fold-ului fără layout shift. hydrate neverdoar pentru conținut cu adevărat static: nu e adnotat, deci nimic nu e redat. Păstrezi@placeholder: trigger-ele hydrate sunt ignorate la navigarea pe client.withIncrementalHydration()e deprecat, iarwithEventReplay()e redundant față de implicit; opt-out-ul ewithNoIncrementalHydration().- Nu împachetezi componente de câțiva KB: chunk-ul și cererea în plus costă mai mult decât economisesc. Folosești stări CSS (
:active,:focus-visible) ca utilizatorul să vadă că click-ul s-a înregistrat.
<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 -->Probabil te întreabă apoi
Ce cere
hydrate whende la părinte și ce se întâmplă cu blocurile dehidratate imbricate?Semnal de alarmă
Să pui
hydrate neverpe ceva cu listeneri sau să scoți@placeholder„pentru că oricum nu se vede”.- Spre deosebire de un
S-11Ce cod nu e SSR-safe și cum previi hydration mismatch-urile într-o echipă? SSR
Spune asta întâi
Orice atinge
window,localStorage, layout-ul sau observerii în constructori, inițializatori de câmpuri,ngOnInitsaueffect()— acestea rulează și pe server. Munca strict de browser merge înafterNextRender/afterRenderEffectsau în spatele unui provider de platformă, iar output-ul randat trebuie să fie identic pe ambele platforme.Apoi intră în detalii
@if (isPlatformBrowser(…))într-un template nu aruncă eroare, dar Angular aruncă DOM-ul de pe server pentru acel bloc și îl reconstruiește — un layout shift. Preferi@defersauafterNextRender.- Cauze de mismatch: manipularea directă a DOM-ului (
innerHTML,appendChild), HTML invalid (<div>în<p>), scripturi externe, un CDN care șterge comentariile (NG0507) și valori caDate.now()care diferă între platforme. - Serverele zoneless nu văd async-ul tău (fetch, SDK-uri, timere): folosești
resource()sauPendingTasks, altfel HTML-ul pleacă gol și browserul cere datele din nou. - Scurgeri de stare: variabilele la nivel de modul și obiectele din
useValuesunt comune tuturor utilizatorilor procesului Node; folosești servicii sauuseFactory. - În echipă: un build SSR în CI, un checklist de review și
ngSkipHydrationpermis doar ca patch temporar și documentat.
Probabil te întreabă apoi
Preferința de temă stă în
localStorage. Cum randezi tema corectă pe server?Semnal de alarmă
if (typeof window !== 'undefined')presărat prin componente, sau tratarea luieffect()ca un „hook doar de browser”.S-12Preiei o aplicație Angular lentă. Care e strategia ta de performanță și cum previi regresiile? Performanță
Spune asta întâi
Întâi măsor, apoi repar cel mai mare blocaj: Core Web Vitals (LCP, INP, CLS) de la utilizatori reali și Profiler-ul din Angular DevTools îmi spun dacă problema e la încărcare, la randare sau un main thread ocupat. Apoi blochez câștigurile cu bundle budgets în CI.
Apoi intră în detalii
- Încărcare: rute lazy,
@deferpentru conținutul greu de sub fold (niciodată elementul LCP),injectAsync()pentru servicii grele folosite rar,NgOptimizedImagecuprioritype imaginea LCP, importuri țintite verificate cusource-map-explorer. - Randare: OnPush + semnale,
trackpe un id stabil, derivări încomputed()în loc de metode în template; pentru zeci de mii de rânduri, virtual scroll din CDK sau paginare — OnPush reduce cât de des randezi, nu cât de mult. - Main thread: calculele continue de peste ~50 ms (parsare, filtrări mari) merg într-un Web Worker; transferi
ArrayBuffer-uri în loc să copiezi date uriașe. - Controlul regresiilor:
budgetsînangular.jsoncare pică build-ul, o verificare de performanță pe paginile cheie la fiecare release și un responsabil pentru cifre.
Probabil te întreabă apoi
INP e prost, dar LCP e bun. Unde te uiți întâi?
Semnal de alarmă
Optimizare fără măsurare, sau „punem SSR și
@deferpeste tot” — inclusiv pe conținutul principal, ceea ce înrăutățește LCP-ul.- Încărcare: rute lazy,
S-13Cum protejează Angular împotriva XSS, unde sunt ușile laterale și care e a doua ta linie de apărare? Securitate
Spune asta întâi
Angular tratează orice valoare legată ca nesigură: interpolarea face escape,
[innerHTML]e sanitizat, schemele URL periculoase devinunsafe:. XSS-ul în aplicațiile Angular vine aproape mereu din ocolirea acestor protecții —bypassSecurityTrust*pe date nesigure,nativeElement.innerHTML,eval— deci a doua linie e un CSP strict cu nonce, plus Trusted Types.Apoi intră în detalii
bypassSecurityTrust*e legitim doar pentru conținut pe care îl construiești și îl controlezi (un URL de embed dintr-un id validat). Un pipe generic „safeHtml” e un XSS reutilizabil; cauți aceste apeluri la fiecare review.- CSP: Angular are nevoie de un nonce pentru stilurile inline —
ngCspNoncepe elementul rădăcină sau token-ulCSP_NONCE;security.autoCspcalculează hash-uri la build, dar acoperă doar scripturile și nu funcționează cu SSR. - Introduci CSP-ul întâi cu
Content-Security-Policy-Report-Only, altfel chunk-urile lazy sau stilurile inline se strică în producție. - CSRF cu cookie de sesiune:
HttpClienttrimite cookie-ulXSRF-TOKENcaX-XSRF-TOKENpe cererile care modifică date, către aceeași origine — dar serverul trebuie să-l seteze și să-l verifice. Orice verificare din frontend se repetă pe server.
Probabil te întreabă apoi
Product vrea să afișeze Markdown scris de utilizatori. Cum o faci în siguranță?
Semnal de alarmă
„Angular sanitizează tot, deci suntem în siguranță”, în timp ce codul are un pipe cu
bypassSecurityTrustHtmlaplicat pe conținut din CMS.S-14Proiectează un interceptor de autentificare cu refresh token. Ce se întâmplă când șase cereri primesc 401 simultan și unde păstrezi token-urile? HTTP
Spune asta întâi
Exact un refresh în desfășurare, partajat de toate cererile care așteaptă (un Observable păstrat cu
shareReplay(1), curățat înfinalize), apoi fiecare cerere originală e reluată cu token-ul nou. Access token-ul în memorie, refresh token-ul într-un cookie HttpOnly, Secure, SameSite — sau un BFF, unde browserul are doar un cookie de sesiune.Apoi intră în detalii
- Varianta naivă „refresh la fiecare 401” face N refresh-uri; cu rotația refresh token-ului se invalidează reciproc, iar unele servere tratează refolosirea ca atac și închid sesiunea.
- Marchezi login/refresh cu un
HttpContextToken(SKIP_AUTH) ca interceptorul să nu se intercepteze singur, atașezi token-ul doar la URL-urile API-ului tău, iar dacă token-ul s-a schimbat deja de când a plecat cererea, doar reîncerci. - Doar 401 declanșează refresh — 403 înseamnă „nu ai voie”. Un refresh eșuat înseamnă logout, curățarea store-urilor și cache-urilor și propagarea logout-ului în celelalte tab-uri (
BroadcastChannel). - La pornire, un
provideAppInitializerîncearcă un refresh înainte de prima rută. Cu un provider OIDC extern, folosești o bibliotecă certificată în loc să scrii singur PKCE și rotația.
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; }Probabil te întreabă apoi
De ce e acceptabil
shareReplay(1)aici, când pe alte stream-uri e o capcană cunoscută?Semnal de alarmă
JWT în
localStoragefără să te gândești la XSS sau CSP, un refresh separat pentru fiecare 401, sau verificări de permisiuni bazate pe decodarea JWT-ului în frontend.S-15Care e strategia ta de tratare a erorilor într-o aplicație Angular? Arhitectură
Spune asta întâi
Pe straturi: erorile previzibile se tratează local, lângă date (un 404 sau 422 afișat de store/componentă), cele transversale central, într-un interceptor (401, 5xx), erorile de randare ale unei zone cu
@boundary(v22.2, developer preview), iar restul ajunge la unErrorHandlerglobal care loghează în telemetrie.Apoi intră în detalii
provideBrowserGlobalErrorListeners()trimitewindow.onerrorși promise-urile respinse netratate laErrorHandler; ții handler-ul simplu și sigur — dacă aruncă, pierzi toate erorile.@boundaryprinde doar erorile de randare, nu pe cele din event handlers sau cod asincron, iar o eroare prinsă tot ajunge raportată laErrorHandler.- Interceptorul aruncă din nou eroarea după tratare (
throwError(() => err)), ca apelanții să poată reacționa; uncatchError(() => of([]))fără logare face bug-urile invizibile. - UX și operare: centralizezi și deduplici toast-urile (nu cinci mesaje pentru o problemă), traduci erorile tehnice în erori de domeniu în adaptor și atașezi context (rută, versiune, remote) fiecărei erori logate.
Probabil te întreabă apoi
Un widget de pe dashboard aruncă la randare și golește toată pagina. Cum îl izolezi?
Semnal de alarmă
Un
try/catchsaucatchErrorîn fiecare componentă care înghite erorile și un toast pentru fiecare, fără logare centrală.S-17Ce strategie de testare ai stabili pentru o echipă: ce intră în teste unit, de componentă și e2e și ce faci mock? Testare
Spune asta întâi
Testezi comportamentul, nu implementarea, și împingi fiecare verificare la cel mai ieftin nivel care o poate prinde: funcții pure și store-uri fără DOM, componente dumb prin input-uri și output-uri, pagini smart cu un store fals și
HttpTestingController, plus câteva fluxuri e2e (Playwright/Cypress) pentru ce iese la iveală doar la integrare. Faci mock la granițe — HTTP și porturi — nu la propriile componente copil.Apoi intră în detalii
- Din v21 runner-ul implicit e Vitest (
@angular/build:unit-test); Karma e deprecat, iar suitele vechi pot fi mutate cu schematic-ul experimentalrefactor-jasmine-vitest. - Teste zoneless:
await fixture.whenStable()în loc defakeAsync, fake timers din Vitest pentru debounce,TestBed.tick()ca să ruleze effect-urile; input-uri ca semnale prininputBinding. - Cu ports & adapters, rulezi o singură suită de teste de contract pe fiecare adaptor; harness-urile din CDK fac testele de componente independente de structura DOM-ului.
- În CI:
nx affectedplus cache pentru viteză, fără cache pentru e2e nedeterministe pe un backend real, iar coverage-ul ca indicator, nu ca țintă.
Probabil te întreabă apoi
Un test e2e instabil pică o dată din zece rulări. Cum îl gestionezi în echipă?
Semnal de alarmă
„100% coverage cu totul mock-uit” — teste care verifică apelarea metodelor private, se strică la fiecare refactorizare și ratează bug-urile reale.
- Din v21 runner-ul implicit e Vitest (
S-18Ce strategie de i18n ai alege: i18n-ul la build din Angular sau o bibliotecă de traduceri la runtime? i18n
Spune asta întâi
@angular/localizeîncorporat, implicit: texte marcate cui18n/$localize, extrase cung extract-i18nși un build optimizat pe fiecare limbă, fără căutare de chei la runtime. O bibliotecă la runtime doar când schimbarea limbii fără reload e cerință de produs sau traducerile vin dintr-un CMS fără deploy.Apoi intră în detalii
- Schimbarea limbii cu i18n-ul încorporat înseamnă reload, chiar și cu
loadTranslations()la runtime, pentru că textele din template sunt evaluate la crearea componentelor. - Pluralele cer ICU, nu
n === 1 ? … : …: româna areone,fewșiother(„20 de fișiere”). Nu concatenezi niciodată propoziții — ordinea cuvintelor diferă între limbi. - Id-uri custom (
@@cart.title) ca traducerile să supraviețuiască modificărilor textului sursă; datele, numerele și moneda prin pipe-uri cuLOCALE_IDșiregisterLocaleData. - Se potrivește cu SSR/prerender (o variantă pe limbă, aleasă după URL), iar cu hidratarea adaugi
withI18nSupport(). Începi din prima zi: i18n adăugat mai târziu înseamnă să vânezi fiecare text scris direct în cod.
Probabil te întreabă apoi
Cum organizezi fluxul cu traducători profesioniști și ții fișierele XLIFF sincronizate între release-uri?
Semnal de alarmă
Formate scrise direct, ca
'dd/MM/yyyy'sautoFixed(2) + ' lei', concatenare de string-uri pentru propoziții și fără id-uri custom pentru mesaje.- Schimbarea limbii cu i18n-ul încorporat înseamnă reload, chiar și cu
S-19Compania voastră își construiește propriul design system în Angular. Cum faci accesibilitatea o proprietate a sistemului, nu a fiecărui ecran? Accesibilitate
Spune asta întâi
Construiești pattern-urile interactive pe Angular Aria (
@angular/aria, din v21): directive headless precum Tabs, Menu, Combobox, Listbox, Tree sau Grid, care dau roluri ARIA corecte, navigare din tastatură și management al focusului, cu zero stiluri, iar echipa deține CSS-ul. Astfel fiecare echipă de produs primește accesibilitatea implicit, în loc să rescrie de mână tab-uri și dropdown-uri.Apoi intră în detalii
- Alegerea stratului: Aria pentru un design system propriu, CDK pentru primitive low-level (overlay, drag & drop, utilitare a11y), Material când vrei rapid un UI gata făcut.
- Stilizezi după starea ARIA pe care o setează directivele (
[aria-selected='true']), ca starea vizuală și cea accesibilă să nu se poată desincroniza. - Compromisuri în alte locuri: virtual scroll scoate din DOM rândurile din afara ecranului (Ctrl+F și cititoarele de ecran nu le văd); preferi paginarea unde contează accesibilitatea completă.
- Proces: verificări din tastatură și cu cititor de ecran în definition of done, verificări automate în CI și review de a11y ca parte din contractul design system-ului.
Probabil te întreabă apoi
O echipă folosește deja Angular Material. Ai migra-o la Aria?
Semnal de alarmă
„Adăugăm atribute
aria-*la final, înainte de release” — accesibilitatea tratată ca un checklist de final în loc de proprietate a componentelor.S-20Conduci upgrade-ul unei aplicații mari de la Angular 21 la 22. Ce breaking changes te îngrijorează și cum îl implementezi? Arhitectură
Spune asta întâi
Câte un major pe rând, cu
ng update @angular/core@22 @angular/cli@22, lăsând migrările să ruleze — de exemplu adaugăChangeDetectionStrategy.Eagerunde componentele nu aveau strategie, deci comportamentul rămâne identic deși OnPush devine implicit. Riscul real e în schimbările de runtime pe care migrările nu le văd complet: backend-ul fetch, moștenirea parametrilor în router și semantica din template.Apoi intră în detalii
- HTTP: fetch e backend-ul implicit, iar
withFetch()e deprecat; progresul la upload cerewithXhr(), iarreportProgressdevinereportUploadProgress/reportDownloadProgress. - Router și template-uri:
paramsInheritanceStrategy: 'always'(rutele copil moștenesc params și data de la toți părinții),?.întoarceundefinedca în TypeScript, un compilator mai strict (input-uri legate de două ori,data-*nu mai leagă input-uri),CanMatchFncerecurrentSnapshot. - Platformă și eliminări: TypeScript 6 și fără Node 20;
ComponentFactoryResolver,ComponentFactoryșiprovideRoutesau dispărut; pentru SSR configureziallowedHostsși scoțiwithIncrementalHydration(). - Implementare: un branch de upgrade ținut verde în CI, bibliotecile și Node/TS actualizate întâi, e2e pe fluxurile critice, apoi un plan ulterior de a scoate
Eagerfeature cu feature, în loc să-l tratezi ca permanent.
Probabil te întreabă apoi
După upgrade, o rută imbricată primește brusc un parametru pe care înainte nu-l primea. Ce s-a schimbat și cum verifici dacă contează?
Semnal de alarmă
Să modifici de mână versiunile din
package.jsonși să sari pesteng update, sau să sari mai multe majore într-un singur pas fără să citești changelog-ul.- HTTP: fetch e backend-ul implicit, iar
Cum răspunzi bine
- O propoziție clară întâi, detaliile doar dacă ți se cer.
- Un exemplu concret dintr-un proiect: „aveam X, am ales Y pentru că Z”.
- Numești compromisul și cazul în care ai alege altfel.
- „Nu știu, dar așa aș afla” când chiar nu știi.
- Definiții recitate fără să spui când le-ai folosi.
- Răspunsuri vechi prezentate ca actuale (NgModule peste tot, Zone.js de la sine înțeles).
- „Depinde”, fără să spui de ce depinde.
- Un API inventat ca să nu rămâi tăcut.