During a technical phone screen for a mid-level Angular role at a Series B logistics startup in early 2026, the interviewer spent most of the fifteen coding minutes on one question: why did a component re-render four times after a single input changed? The candidate could recite the definition of OnPush from a course. He'd never actually opened Angular DevTools' profiler to watch a redundant render happen in real code. He didn't get the offer.
Angular interview questions in 2026 split into two buckets that don't overlap as much as people expect. One tests whether you understand why Angular re-renders what it re-renders, change detection, RxJS timing, the signals model. The other tests whether you've actually built something with the framework past a tutorial, dependency injection, forms, routing guards. Angular was used by 19.8 percent of professional developers in the 2025 Stack Overflow Developer Survey, less than half of React's 46.9 percent (Stack Overflow, 2025). That gap changes how the interview runs. Companies hiring for Angular know they're fishing in a smaller, more experienced pool, so interviewers skip syntax trivia and go straight for whether you understand the reactivity model, because they've learned that a candidate who writes *ngFor correctly can still not know why their app freezes on a long list.
Here's an opinion, and it might be wrong: the standalone components migration quietly killed the easiest warm-up question in Angular's interview history. "What's the difference between a module and a component" used to be a reliable icebreaker. Since Angular 19 made standalone: true the default for every generated component, a lot of candidates have never touched an NgModule outside inherited legacy code, while plenty of interviewers still open with module questions anyway. That mismatch burns good candidates for no good reason, and I don't think most interview panels have updated their question banks to reflect it. This page covers the Angular developer interview questions that come up across real 2026 loops, organized by topic, components and binding, directives, dependency injection, RxJS, change detection, modules and routing, and forms, with a look at where signals are changing the older answers.
Components, templates, and data binding
Every loop opens here, even for senior candidates. It's a warm-up, but a shaky answer sets a bad tone for the rest of the interview.
Easy questions
15A component is a directive that carries its own template, styles, and view. Every component is a directive under the hood, the @Component decorator is itself built on @Directive, but not every directive is a component. Attribute directives change how an existing element looks or behaves without owning a template, and structural directives add or remove elements from the DOM entirely.
Interviewers use this to check whether you treat directive as the real base abstraction rather than an implementation detail. Naming all three categories, component, structural, attribute, without hesitating usually clears this one fast.
Interpolation, {{ value }}, is one-way from component to view, for rendering text. Property binding, [property]="expr", is one-way from component to a DOM property or a child's @Input(). Event binding, (event)="handler()", is one-way from view to component, for user actions. Two-way binding, [(ngModel)]="value", the "banana in a box" syntax, combines property and event binding into a single expression.
Interviewers almost always follow up by asking you to write out what [(ngModel)]="value" desugars to by hand: [ngModel]="value" (ngModelChange)="value = $event". If you can't produce that, you've memorized the syntax without understanding it.
<p>{{ username }}</p>
<input [value]="username" />
<button (click)="save()">Save</button>
<input [(ngModel)]="username" />Structural directives, *ngIf, *ngFor, or the newer @if/@for control-flow blocks, add, remove, or repeat elements in the DOM. Attribute directives, ngClass, ngStyle, or a custom [appHighlight], change the appearance or behavior of an element that's already there without touching the structure around it.
Interviewers usually push on the asterisk: *ngIf is shorthand for <ng-template [ngIf]="condition">. Explaining that the asterisk desugars into an ng-template wrapper gets you past the surface-level version of this question.
A service is a plain TypeScript class, usually decorated with @Injectable(), that holds logic or state multiple components might need, an HTTP client wrapper, shared form state, an auth check. Putting that logic in a service instead of a component keeps components focused on presentation and lets Angular's injector manage the service's lifecycle instead of every component instantiating its own copy.
Interviewers rarely stop here. This question is almost always a setup for the DI hierarchy question that follows.
A Promise resolves once and starts executing immediately when it's created, it's eager, and you can't cancel it. An Observable can emit multiple values over time, it's lazy, nothing runs until something subscribes, it's cancellable via unsubscribe, and it comes with an operator library for transforming streams declaratively.
The lazy part trips people up in production just as often as in interviews. "Why didn't my HTTP call fire" is a real bug, and the answer is almost always that nobody subscribed. Interviewers ask this to see if you've actually hit that bug, not whether you can define the two terms.
ngOnChanges fires before ngOnInit and again whenever a bound @Input() reference changes. ngOnInit fires once, after Angular sets the initial input values. ngDoCheck fires on every change detection run, for custom check logic. ngAfterContentInit and ngAfterContentChecked fire after content projected through ng-content is initialized and checked. ngAfterViewInit and ngAfterViewChecked fire after the component's own view, children included, is initialized and checked. ngOnDestroy fires right before Angular tears the component down.
The detail interviewers actually probe: ngOnChanges only fires on a reference change, not a mutation. Push a new item into an array bound as an @Input() and ngOnChanges stays silent, because the reference never changed. That's a real production bug source, not a trivia trap.
Lazy loading means a feature's code only downloads when a user actually navigates to it, instead of bundling everything into the initial JavaScript payload. For an app with a lot of rarely visited routes, an admin panel, a settings page nobody opens, that keeps the first load small and time-to-interactive fast.
In modern Angular you implement it with loadComponent for a single standalone component, or loadChildren for a full route tree. Interviewers mostly want to hear that you know why it matters, not just the syntax.
Template-driven forms build the form model behind the scenes from directives in the template, ngModel, ngForm, which makes very simple forms fast to write but harder to unit test, since the form model isn't directly accessible from the component class. Reactive forms define the structure explicitly in the component class with FormGroup and FormControl, and the template just binds to it, which scales better once you need dynamic validation or more than a handful of fields.
Most production Angular shops default to reactive forms once a form gets past three or four fields or picks up any conditional validation. Interviewers want to hear that opinion and the reasoning behind it, not a textbook definition of both.
A pipe transforms a value for display without touching the underlying data. Angular ships built-in pipes like date, currency, uppercase, and json, and you apply them with the pipe operator inside interpolation, for example {{ order.total | currency:'USD' }} or {{ user.createdAt | date:'mediumDate' }}.
You can chain pipes left to right, so {{ name | lowercase | titlecase }} runs lowercase first then titlecase on the result. You can also write your own with the @Pipe decorator and a transform method when a built-in one doesn't fit.
The older pattern calls platformBrowserDynamic().bootstrapModule(AppModule), where AppModule declares components, imports other modules, and registers providers. Every component needs to belong to some module's declarations array before it can be used.
Standalone bootstrapping calls bootstrapApplication(AppComponent, { providers: [...] }) directly against a component marked standalone: true. There's no root module at all, routing comes from provideRouter(routes), HTTP from provideHttpClient(), and each component imports the directives, pipes, or other components it actually needs in its own imports array instead of inheriting a module's exports.
Content projection lets a component render markup that its parent passes in between its opening and closing tags, similar to a slot in other frameworks. Inside the child's template you place
For multiple slots you use the select attribute, like
The constructor is plain TypeScript class instantiation, and Angular only uses it to inject dependencies through the parameter list. At that point none of the component's @Input() bindings have been set yet, so reading this.someInput inside the constructor gives you undefined.
ngOnInit fires right after Angular runs the first ngOnChanges, once all bound inputs have real values, which is why API calls, subscriptions, and any logic depending on an @Input() belong there instead of in the constructor.
A template reference variable is created with a hash, like #box on a DOM element or #timer on a component tag, and it gives you a handle to that element or component instance usable anywhere else in the same template.
For a plain element, lets you write (click)="log(box.value)" on a nearby button. For a component,
The async pipe subscribes to an Observable or a Promise for you, unwraps the emitted value directly into the template with {{ data$ | async }}, and marks the component for check whenever a new value arrives.
The real benefit is cleanup: it automatically unsubscribes when the component is destroyed, so you skip writing a manual subscription, an ngOnDestroy, and the takeUntil boilerplate that goes with it, and you avoid the whole class of bugs where a forgotten unsubscribe keeps firing callbacks against a component that's already gone.
They hold build-time configuration values, things like apiUrl, a feature flag, or a production boolean, and the Angular CLI swaps between them using fileReplacements defined per build configuration in angular.json, so ng build --configuration=production pulls in environment.prod.ts instead of environment.ts.
They are not a place to store real secrets. Everything in either file ends up bundled into the client-side JavaScript, visible to anyone who opens dev tools, so API keys that actually need to stay private belong on a server, not in an environment file.
Medium questions
25Yes. The banana-in-a-box syntax is a compiler convention, not something exclusive to ngModel. Define an @Input() for the value and a matching @Output() named exactly the input name plus "Change", an EventEmitter. Any consumer can then write [(value)]="parentProp" and Angular wires it up the same way it wires ngModel.
This question separates candidates who've only consumed Angular's built-in two-way binding from ones who've actually built a reusable component. Interviewers watch for whether you get the naming convention right the first time, valueChange, not changeValue.
@Component({
selector: 'app-counter',
standalone: true,
template: `<button (click)="increment()">{{ value }}</button>`
})
export class CounterComponent {
@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();
increment() {
this.value++;
this.valueChange.emit(this.value);
}
}
// usage: <app-counter [(value)]="count"></app-counter>Inject TemplateRef and ViewContainerRef in the constructor. In the setter for the directive's input, call viewContainerRef.createEmbeddedView(templateRef) when the condition is false, and viewContainerRef.clear() when it's true, the inverse of *ngIf's logic.
This tests whether you understand what *ngIf is actually doing rather than treating it as magic. A candidate who's written one custom structural directive answers this in thirty seconds. One who hasn't usually needs a couple of prompts to land on createEmbeddedView.
@Directive({ selector: '[appUnless]', standalone: true })
export class UnlessDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}@if, @for, and @switch, introduced in Angular 17, are block-level template syntax compiled directly by the Angular compiler instead of resolved through the structural-directive and TemplateRef machinery. That gives better type narrowing inside strict-mode templates and, by the Angular team's own account, faster list rendering.
The detail that trips people up: @for requires a track expression. It isn't optional the way *ngFor's trackBy was. Candidates who migrated a real app to the new control flow know this because the compiler won't let them skip it. Candidates who only read a blog post about it usually don't.
providedIn: 'root' registers one app-wide singleton, created lazily on first use, and tree-shakable, if nothing in your app ever injects it, it never ships in the bundle. Providing the same service in a component's providers array instead creates a new instance scoped to that component and everything below it in the tree.
Interviewers probe whether you know a child injector shadows its parent rather than merging with it. Re-provide a token further down the tree and every descendant gets that lower instance, siblings included, the ancestor's instance becomes invisible to that subtree.
InjectionToken<T> lets you inject values that aren't classes, configuration objects, primitives, or anything shaped by an interface. Interfaces don't exist at runtime once TypeScript compiles, so you can't use one directly as a DI token the way you can a class.
Common use case: environment configuration or a feature-flag object injected into several unrelated services. Interviewers sometimes ask you to spot the bug in code that tries to inject an interface directly, a mistake that only shows up at runtime, which is exactly why it makes a good interview question.
switchMap cancels the previous inner observable the moment a new value arrives, the standard choice for a search-as-you-type box where stale results don't matter. mergeMap runs every inner observable concurrently with no cancellation, useful for firing off several independent requests in parallel. concatMap queues inner observables and runs them strictly one at a time, in order, the right call when write order actually matters. exhaustMap ignores new source emissions while an inner observable is still running, which makes it the correct fix for a double-submit bug on a form button.
Most interviewers don't ask for all four definitions in the abstract. They give you a scenario, a user double-clicks submit and the form posts twice, and want you to land on exhaustMap. It's the operator most candidates forget exists, and it's usually exactly the right answer.
A plain Subject has no initial value and only multicasts to whoever's already subscribed, a late subscriber gets nothing until the next emission. A BehaviorSubject requires an initial value, always exposes the current value through .value, and immediately hands new subscribers the latest value on subscribe. A ReplaySubject buffers a configurable number of past values and replays them to new subscribers regardless of when they joined.
Interviewers usually want to hear that BehaviorSubject is what most real state services actually use, because a component mounting after the initial data load still needs the current state immediately, not the next emission that may never come.
Under the default strategy, Angular checks every component in the tree, top to bottom, after almost any async event zone.js has patched, a click, an HTTP response, a setTimeout, a resolved promise, regardless of whether that specific component's data changed. Under OnPush, a component only gets checked when an @Input() reference changes, an event originates from inside the component's own template, an observable bound through the async pipe emits, or you manually call markForCheck() or detectChanges().
Plenty of candidates can define OnPush accurately and still can't explain why a mutated array doesn't trigger a re-check. That's the actual differentiator interviewers listen for, not the definition.
Not in the same way. Per Angular's own signals guide, when you read a signal inside an OnPush component's template, Angular tracks that signal as a dependency automatically, and when the signal's value changes, Angular marks the component for update on its own (Angular docs, Signals). You stop reasoning about reference equality on plain objects and start reasoning about which signals a given template actually reads.
I'll admit I don't have solid data on how fast production codebases are migrating existing OnPush components to signals, most of what's public is migration guides, not adoption numbers. What I'm more confident about: interviewers at companies that have already migrated ask candidates to explain the difference between a signal's automatic dependency tracking and OnPush's manual reference-check model, and "they're basically the same thing" is the wrong answer. One tracks dependencies at read time. The other checks references at the input boundary.
NgModule-based: { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }. Standalone: { path: 'admin', loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent) }.
The standalone version skips an entire module wrapper, one less file, one less layer of indirection, and because there's no module metadata to include, the resulting lazy chunk is usually a little smaller too. Interviewers sometimes ask why that's not purely a boilerplate reduction, it's a real, if small, bundle-size difference.
CanActivate decides whether a route can be entered at all, the classic auth check before navigation completes. CanDeactivate decides whether a user can leave the current route, an unsaved-changes warning on a form page. CanMatch decides whether a route configuration even matches in the first place, before Angular picks it, useful for routing to one of two lazy chunks based on a feature flag without a redirect.
Since Angular 14, guards are usually written as plain functions using inject(), not injectable classes implementing an interface. Candidates who learned Angular before that still write class-based guards out of habit, and a fair number of interviewers will hand you one and ask you to convert it live.
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn() ? true : router.parseUrl('/login');
};A custom sync validator is a function, (control: AbstractControl) => ValidationErrors | null, that runs on every value change and returns either an error object or null. An async validator returns an Observable or Promise instead, used for things like checking username availability against a backend, Angular waits for it to resolve before moving the control from PENDING to VALID or INVALID.
One detail interviewers like to check: Angular doesn't debounce or cancel stale async validator requests for you. If you want that, you build it yourself, usually with a switchMap inside the service call the validator wraps.
function forbiddenNameValidator(forbidden: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const isForbidden = forbidden.test(control.value);
return isForbidden ? { forbiddenName: { value: control.value } } : null;
};
}A pure pipe, which is the default, only re-runs its transform when the reference of its input changes, a new primitive value or a new object/array reference. That makes it cheap since it does not run on every single change-detection cycle, only when something actually swapped.
An impure pipe, declared with pure: false in the @Pipe decorator, runs on every change-detection check regardless of reference equality. You'd reach for that if you're filtering or sorting an array that gets mutated in place, push() instead of a new array, since a pure pipe would never notice the mutation. The tradeoff is real performance cost on a large list, so it's usually better to keep the array immutable and let a pure pipe (or a computed signal) do the work instead of defaulting to impure.
static: true resolves the query before the first change-detection run, so it's available in ngOnInit, but only works if the queried element isn't inside a structural directive like *ngIf or *ngFor, since Angular has to know it exists at that exact point without running change detection first.
static: false, which is the current default, resolves after ngAfterViewInit and keeps updating on subsequent checks, which is what you need when the element you're querying might not exist yet, wrapped in an *ngIf that starts false, for instance. Getting this backwards shows up as either undefined in ngOnInit or a query that never updates when the underlying element toggles in and out.
providers is visible to the component's own template and to any content that's projected into it through ng-content. viewProviders is only visible to the component's own view, not to whatever markup a consumer passes in as projected content.
You'd reach for viewProviders when you want to hide an implementation-detail service from consumers, so a child component projected inside your component can't accidentally resolve and depend on a service that's really just an internal collaborator, keeping your public API surface smaller.
Normally registering a provider against a token overwrites any previous provider for that same token. multi: true tells the injector to collect every provider registered against that token into an array instead, so nothing gets silently replaced.
Angular uses this internally for HTTP_INTERCEPTORS, letting you register an auth interceptor, a logging interceptor, and an error-handling interceptor independently and have all three run in a chain, and for NG_VALIDATORS and NG_VALUE_ACCESSOR, letting a form control pick up multiple validators or custom value accessors registered by different directives on the same element.
Interceptors registered as multi providers form a pipeline. Each one receives the request and a next handler, calls next.handle(req), and the order they were registered in is the order they run for the outgoing request, then reversed for the response coming back.
Order matters concretely with auth: an interceptor that attaches a bearer token needs to run before a logging interceptor, otherwise the log captures a request without the header. It also matters for retry logic on a 401, since an interceptor that catches a 401 and tries to refresh the token has to sit after the auth interceptor in the chain, and you have to be careful it doesn't retry the refreshed request through the same interceptor again and loop forever.
The classic pattern is a private destroy$ = new Subject
takeUntilDestroyed(), from @angular/core/rxjs-interop, does the same job without the boilerplate. It hooks into the component's DestroyRef directly, so you just write .pipe(takeUntilDestroyed()) inside the constructor or another injection context and skip writing ngOnDestroy entirely. Forgetting either pattern means the subscription outlives the component, still firing its callback against a torn-down view, which can show up as errors touching destroyed DOM or an ExpressionChangedAfterItHasBeenCheckedError from state changing after the fact.
Without trackBy, Angular's default tracking compares by object identity. If the array reference gets replaced, a new array coming back from a polling API even if most of the rows are unchanged, Angular destroys and recreates every single DOM node and child component in that list instead of reusing what's already there. That kills performance on a long list and also loses any local state in the list items, scroll position, focus, an open accordion, an in-progress animation.
A trackBy function like (index, item) => item.id gives Angular a stable key to match old items against new ones, so it only creates or removes nodes for rows that actually changed and patches the rest in place.
It happens in development mode when a bound value changes during the same change-detection pass after Angular already checked and rendered it, most often from setting a property inside ngAfterViewInit or ngAfterContentInit, or a child component's lifecycle hook reading and mutating something on its parent after the parent already finished rendering that value.
The real fix is to move the state change earlier, into ngOnInit if it doesn't depend on view children, or to defer it a tick with Promise.resolve().then(() => ...) or setTimeout so it lands in a fresh change-detection cycle. You can also explicitly call this.cdr.detectChanges() at the end of the lifecycle hook to force the view to re-check immediately with the new value. The trap to avoid is treating this as a dev-mode annoyance to silence, since the underlying bug still exists in production, Angular just doesn't throw there, and users end up seeing stale or flickering data instead.
Renderer2 abstracts DOM manipulation behind an API that works across whatever platform your app actually runs on, a real browser, Angular Universal on the server where there's no document at all, or a web worker. Code that calls elementRef.nativeElement.style.color = 'red' directly assumes a real DOM exists and breaks the moment that assumption isn't true.
It also keeps the change happening through Angular's own tracked mechanisms rather than bypassing them entirely, which matters for testability, since a test harness can mock Renderer2 calls but has a harder time intercepting raw nativeElement mutations, and it avoids some of the direct-DOM patterns that are easier to accidentally turn into an XSS vector.
The validator has to live on the parent FormGroup rather than on either individual control, since it needs access to both sibling values at once.
function passwordMatch(group: AbstractControl): ValidationErrors | null { const p = group.get('password')?.value; const c = group.get('confirmPassword')?.value; return p === c ? null : { mismatch: true }; }
You pass it as the second argument to the FormGroup constructor, or add it to the group's validators array if you're using FormBuilder. The resulting error surfaces on the group, not on either control, so the template checks form.errors?.['mismatch'] rather than form.get('confirmPassword')?.errors.
Zone.js patches asynchronous browser APIs, setTimeout, XHR, DOM event listeners, promises, so Angular can tell when an async task finishes and automatically run change detection afterward. That's what makes {{ }} bindings update without you manually calling anything, but it also means every patched callback triggers a change-detection pass across the whole component tree by default.
That gets expensive for high-frequency work, a mousemove handler, a canvas animation loop, a websocket firehose, a third-party library polling on an interval, where you don't want a full tree check on every single tick. ngZone.runOutsideAngular(() => { ... }) runs that code without triggering change detection at all, and then you call ngZone.run(() => { this.value = x; }) only at the moment you actually need the template to reflect a new value.
HostBinding ties a property, attribute, style, or class on the element the directive is attached to, back to a field in the directive class, and HostListener wires up a DOM event on that same host element to a method, without you having to inject Renderer2 or ElementRef and wire it manually.
@Directive({selector: '[appHighlight]'}) export class HighlightDirective { @HostBinding('style.backgroundColor') bgColor = ''; @HostListener('mouseenter') onEnter() { this.bgColor = 'yellow'; } @HostListener('mouseleave') onLeave() { this.bgColor = ''; } }
Applied as
computed() creates a derived signal that Angular tracks automatically by watching which signals you read inside the function, and it's memoized, only recalculating when one of those dependencies actually changes. A plain getter looks similar in a template but recomputes on every single change-detection check that touches it, whether or not anything it depends on actually changed.
effect() runs a side effect whenever any signal it reads changes, in the same spirit as subscribing to a combineLatest of several observables, but there's no subscription object to manage, no completion to worry about, and it's synchronous by design so you don't get the glitching you can see with combineLatest emitting intermediate states while multiple sources update in the same tick. The practical tradeoff is that signals fit local, synchronous state well, while RxJS still wins for anything genuinely spread over time, an HTTP call, debounced input, a websocket stream, which is why most real apps end up mixing the two through toSignal and toObservable rather than picking one exclusively.
Hard questions
12No. Because each component created its own provider entry, Angular instantiates a separate instance per component, attached to that component's own element injector. Same class, same decorator, zero shared state, unless the provider moves up to a common ancestor, a shared route, or root.
This is the question that separates candidates who've memorized "singletons are app-wide" from ones who've actually debugged a bug caused by an accidental extra provider entry. It happens more than you'd think, someone copies a providers: [MyService] line into a component that didn't need it, and two instances of what was supposed to be shared state quietly stop talking to each other.
OnPush compares inputs by reference. array.push() mutates the array in place, the reference the child received never changes, so Angular has nothing to flag and skips the check entirely. Two real fixes: reassign a new array from the parent, parentArray = [...parentArray, newItem], so the reference actually changes, or inject ChangeDetectorRef into the child and call markForCheck() manually after the mutation.
Interviewers want the immutability answer first. The markForCheck fix works, technically, but it's patching around the actual problem rather than fixing it, and a candidate who reaches for it as the only answer usually hasn't internalized why OnPush exists.
// Won't trigger an OnPush re-check:
this.items.push(newItem);
// Will trigger it:
this.items = [...this.items, newItem];Instead of monkey-patching every async browser API to figure out when something might have changed, zoneless mode (enabled with provideZonelessChangeDetection()) relies on explicit notification. Writing to a signal automatically marks every view that reads it as dirty, and Angular schedules a check through the application's own coalesced scheduler, the async pipe emitting a new value does the same, and calling markForCheck() on a ChangeDetectorRef still works as a manual escape hatch.
The practical change is that any code relying on zone.js implicitly catching an async callback stops working. A plain setTimeout that mutates a regular class property, with nothing wrapped in a signal and no explicit markForCheck() call, used to trigger a check because zone.js noticed the timer fired, but under zoneless nothing notices, and the view silently goes stale. Migrating an existing component means moving its state onto signals or auditing every place it mutates state outside of a signal write, an input binding, or an event handler, and third-party libraries that assume zone.js patched globals, some older chart or drag-and-drop libraries, can behave inconsistently until they're updated or wrapped explicitly.
Hydration assumes the DOM Angular renders on the client structurally matches what the server already sent down, so it can reuse existing nodes and attach event listeners instead of tearing everything down and rendering fresh. That assumption breaks whenever server and client actually produce different markup for the same initial state.
A common way to break it is reading window, document, localStorage, or navigator directly inside a constructor or ngOnInit without guarding with isPlatformBrowser(), since none of those exist on the server, so the server either throws, or the component renders different output there than it does on the client. Another one is an *ngIf condition driven by something non-deterministic between renders, Date.now() or Math.random() evaluated separately on server and client. A third is a third-party widget that injects DOM nodes directly, a chart library appending its own canvas element outside Angular's rendering, which the server never produced, so hydration can't reconcile that subtree and falls back to destroying and rebuilding it on the client, which both loses the performance benefit and can cause a visible flash of content.
input() replaces @Input() with a readonly signal, so you read it by calling it, name() instead of this.name, and anything derived from it can live in a computed() that recalculates automatically and only when that input actually changes. output() replaces @Output()/EventEmitter with a slightly different emitting API internally, but the parent still consumes it with the same (event)="..." binding, so that side of the API doesn't change for consumers.
For the RxJS piece, toSignal(someObservable$, { initialValue: ... }) turns a stream into a signal you can read synchronously inside a computed() or directly in the template, and toObservable(someSignal) does the reverse when a signal's value needs to feed into an RxJS pipeline, debounceTime, switchMap, combineLatest with another stream. The gotcha worth knowing: toSignal unsubscribes automatically when the component's injection context is destroyed, which is the behavior you want, but if the source observable doesn't emit synchronously and you skip initialValue, the signal starts out undefined, which can break template code or a computed() that assumes a real value is present immediately on first render.
function retryWithBackoff
Usage looks like this.http.get(url).pipe(retryWithBackoff(3, 500)).subscribe(...). RxJS 7's retry operator takes a config object where delay controls both whether and how long to wait before the next attempt, since returning an observable from delay retries, and returning a thrown error via throwError stops retrying immediately.
The important edge case is not retrying blindly on every failure. A 400 or a 401 means the request itself is wrong or unauthenticated, retrying it five times with backoff just delays the inevitable failure and wastes the user's time, so the delay callback checks the status code and rethrows immediately for anything that isn't a network failure, a timeout, or a 5xx/429, which are the cases where retrying actually has a chance of succeeding.
Angular auto-sanitizes any value bound into a context like innerHTML, so [innerHTML]="someString" strips out script tags, inline event handler attributes, and javascript: URLs by default, even if that string came from an untrusted source, at the cost of also stripping markup Angular doesn't trust, an inline svg, a video embed src, that you actually intended to keep.
bypassSecurityTrustHtml, or bypassSecurityTrustUrl/Script/ResourceUrl, tells Angular to trust the value completely and skip sanitization entirely. That's only safe when the string is fully controlled server-side and never contains raw user-submitted content, rendering a CMS article body that already passed through a server-side sanitizer like DOMPurify before it reached your API, for example.
The real risk shows up the moment you use it on anything an end user can influence, a comment field, a profile bio, a URL query parameter reflected back into the page, a file name someone uploaded. That's a direct stored or reflected cross-site scripting vector, arbitrary script running in the browser session of every other user who views that content, not just the one who submitted it.
@Injectable({ providedIn: 'root' }) export class SelectivePreloadingStrategy implements PreloadingStrategy { preload(route: Route, load: () => Observable
Register it with provideRouter(routes, withPreloading(SelectivePreloadingStrategy)), then mark individual routes with a data flag, { path: 'reports', loadChildren: () => import('./reports/reports.routes'), data: { preload: true } }.
PreloadAllModules preloads every lazy route in the background right after the initial load finishes, regardless of how likely a user actually is to visit it, which wastes bandwidth loading a rarely used admin settings section on every single visit. NoPreloading, the default, only loads a lazy route the moment someone navigates to it, so the very first visit to any lazy section pays the full download and parse cost with no head start. A selective strategy lets you preload the routes people actually click next most of the time, a dashboard, a reports section, while skipping the ones that are rarely visited, trading a bit of extra background bandwidth for noticeably faster perceived navigation on the routes that matter.
Install the Angular DevTools browser extension and use its Profiler tab to record a timeline while you reproduce the jank, typing in the search box for a few seconds. The recording shows, per component, how long each change-detection check took and how many times it ran during that window. A search-box jank case typically shows a handful of components getting checked hundreds of times a second, because they're not OnPush and sit somewhere in the tree where every keystroke's event bubbles into a full check starting from the root.
Once you've located the offenders, a few fixes usually apply: switch heavy list or table components to ChangeDetectionStrategy.OnPush so they only get checked when their inputs actually change reference, add trackBy if the jank comes from list rendering specifically, debounce the search input's valueChanges with debounceTime(200) and distinctUntilChanged() before it drives anything expensive instead of firing a full pipeline on every keystroke, and audit the template for a method call or a pipe invoked directly, {{ getFilteredItems() }}, since that reruns on every single change-detection check even under OnPush, whereas a memoized computed signal or a precomputed property only recalculates when its actual dependencies change.
Module Federation lets a host application load a remote Angular app's bundle at runtime and mount one of its exposed components, which is genuinely useful for letting separate teams deploy independently on their own schedule. The catch is that Angular's version has to line up closely enough between the host and every remote, since Angular's internal APIs aren't guaranteed stable across major versions, and a host on Angular 17 loading a remote still built on Angular 15 can break in subtle ways around signals or standalone component APIs that the older build doesn't know about.
Shared singleton state is the harder problem in practice. RxJS, zone.js, and Angular's core packages need to be configured as eager, singleton shared dependencies in the federation config, otherwise you end up with two separate zone.js instances patching the same global APIs independently, which shows up as change detection firing inconsistently between the host shell and the remote content, or two separate Router instances fighting over ownership of the same URL.
Routing across the boundary, a link inside the remote that needs to navigate somewhere in the host's own route tree, and CSS isolation, a remote's global styles bleeding into the host or the other way around since Angular's ViewEncapsulation doesn't extend across a Module Federation boundary automatically, are the two things that most often blow up the timeline on a project like this once it's actually in production.
A FormArray holds a variable number of FormGroup instances. Push new groups with formArray.push(this.fb.group({...})) and remove one with formArray.removeAt(index). In the template, loop over formArray.controls, bind each row to its own FormGroup, and expose typed getters on the component instead of indexing into formArray.controls directly inside the template.
Interviewers use this to see whether you've actually built a form like this or only read the docs on FormArray. The indexing-directly-in-the-template version is the sloppy one most people write under time pressure, and it's the version that breaks the moment someone reorders the array.
Start in Chrome DevTools' Memory tab. Take a heap snapshot, perform the same repeated action several times, navigate to a page and back ten times, open and close a modal ten times, then take a second snapshot and diff them. A growing count of detached DOM tree nodes between snapshots is a strong signal that something is still holding a reference to DOM that Angular already destroyed.
From there, cross-check against the usual Angular culprits: a subscription set up in ngOnInit against a long-lived source, a singleton BehaviorSubject, a router events stream, a websocket, without takeUntil or takeUntilDestroyed; an addEventListener added directly on window or document without a matching removeEventListener in ngOnDestroy; a third-party widget, a chart library, a map component, that needs an explicit .destroy() call nobody wired up; and a setInterval whose closure captures this and never gets cleared. Renderer2's renderer.listen() also returns an unsubscribe function that has to actually be invoked, which is easy to forget since it doesn't look like a typical subscription. Fix one candidate at a time and re-snapshot after each change so you can confirm retained size actually drops instead of assuming the fix worked.
How to prepare for an Angular developer interview in 2026
Skip the flashcard approach. Angular interview questions reward people who've watched their own code misbehave, a change detection cycle that ran twice, a subscription that leaked, a form that wouldn't validate the way the spec said it should. Build something with a genuinely dynamic form, a multi-step wizard with conditional fields is enough. Open Angular DevTools' profiler on a real app and watch what actually triggers a change detection pass, do that once and OnPush stops being an abstract idea. Then work through the RxJS operator decision tree until switchMap versus exhaustMap versus concatMap is instinct rather than something you reconstruct under pressure.
Across Angular-focused mock interviews on LastRoundAI, a pattern shows up over and over: candidates can usually describe OnPush correctly the first time they're asked, and then fumble the very next question, why didn't this specific re-render happen? The gap isn't knowledge, it's translating a definition into a live bug. The candidates who do well treat every follow-up as a chance to reason out loud about the actual mechanism, reference equality, dependency tracking, whatever it is, instead of restating the definition they already gave. That's a coachable skill, and it's most of what a practice round is actually good for.
On standalone components specifically: since Angular 19 made them the default for every new component, generated with the CLI, and NgModules stick around only for teams that opt back in explicitly (InfoWorld, 2024), it's worth knowing whether the team you're interviewing with has actually migrated or is still sitting on a pre-19 NgModule codebase. Ask the recruiter. It changes which questions you should expect, and it's a fair thing to ask before the loop starts, not during it.
Get the reps in before the real thing
Reading answers to Angular interview questions is not the same as defending them under a follow-up question. LastRoundAI's mock interview mode runs live coding and behavioral rounds with real-time feedback in your browser, and the free plan includes 15 credits a month that reset monthly rather than stockpiling, so there's no reason to save them for later.
Once the answers are solid, the slower part of the job hunt is usually just getting in front of enough companies. Auto-Apply queues tailored applications to frontend and Angular-heavy roles for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and you approve every application before anything goes out. Nothing sends on your behalf without you looking at it first.
Questions about either product go to contact@lastroundai.com, that's the only inbox we check.

