Angular MCQs multiple-choice questions with answers & explanations
All 32 Angular quiz questions on one page. Pick an answer in your head, then open Show answer to check it and read why. Want a score and a timer? Take them as a quiz instead.
- 1.mid
The parent's Rename button is clicked. What does the OnPush card show afterwards?
@Component({ selector: 'app-user-card', changeDetection: ChangeDetectionStrategy.OnPush, template: `<p>{{ user().name }}</p>`, }) export class UserCard { user = input.required<{ name: string }>(); } // Parent template: <app-user-card [user]="user" /> <button (click)="rename()">Rename</button> // Parent class: user = { name: 'Ada' }; rename() { this.user.name = 'Grace'; }- AGrace: the click ran change detection for the whole app
- BStill Ada: the input reference didn't change, so the OnPush card is never marked for check
- CAngular throws ExpressionChangedAfterItHasBeenCheckedError
- DGrace, but only in development mode
Show answer
Answer: B (Still Ada: the input reference didn't change, so the OnPush card is never marked for check)
An OnPush component is refreshed when an input receives a new value (by reference), an event handler in its template fires, a signal its template reads changes, or it's explicitly marked for check. Mutating
user.namekeeps the same object, so the input never changes. Replace the object instead:this.user = { ...this.user, name: 'Grace' }, or keep it in a signal and useupdate. - 2.mid
What is logged?
const count = signal(1); const double = computed(() => { console.log('computing'); return count() * 2; }); count.set(2); count.set(3); console.log(double()); console.log(double());- Acomputing, 6, 6
- Bcomputing, computing, computing, 6, 6
- Ccomputing, 6, computing, 6
- D6, 6
Show answer
Answer: A (computing, 6, 6)
computedis lazy: nothing runs until it's first read, so the twosetcalls don't compute anything. The first read computes and caches 6; the second read returns the cached value becausecounthasn't changed since. - 3.hard
What is logged?
const user = signal({ name: 'Ada' }); const greeting = computed(() => `Hi ${user().name}`); console.log(greeting()); user().name = 'Grace'; console.log(greeting());- AHi Ada, Hi Grace
- BHi Ada, then an error because signal values are frozen
- CHi Ada, Hi Ada
- DHi Grace, Hi Grace
Show answer
Answer: C (Hi Ada, Hi Ada)
Mutating the object held by a signal doesn't notify anything, so
greetingkeeps returning its memoized value. Produce a new object instead:user.update(u => ({ ...u, name: 'Grace' })). Signals compare values withObject.isby default. - 4.mid
Where in a component class can you call
inject(HttpClient)without any extra setup?- AInside
ngOnInit - BInside a
setTimeoutcallback started from the constructor - CIn a field initializer or the constructor
- DAnywhere, as long as HttpClient is provided in root
Show answer
Answer: C (In a field initializer or the constructor)
inject()only works in an injection context: constructors, field initializers, provider factories, functional guards, resolvers and interceptors, or code wrapped inrunInInjectionContext. Calling it later, e.g. inngOnInitor a callback, throws NG0203. Inject into a field (private http = inject(HttpClient)) and use the field anywhere. - AInside
- 5.hard
CartServiceis@Injectable({ providedIn: 'root' }), andProductPanelalso declaresproviders: [CartService]. The page shows twoProductPanels, and the header (outside the panels) injectsCartServicetoo. How many instances exist?- A1
- B2
- C3
- DNone: providing a root service again is a compile error
Show answer
Answer: C (3)
Each
ProductPanelinstance gets its own element injector with its ownCartService, shared by that panel's children. The header isn't inside a panel, so its lookup falls through to the root injector and gets the app-wide singleton: two panel instances plus one root instance. - 6.mid
After
add()runs, the displayed total doesn't change. Why?@Pipe({ name: 'total' }) export class TotalPipe implements PipeTransform { transform(items: { price: number }[]): number { return items.reduce((sum, item) => sum + item.price, 0); } } // Template: <p>Total: {{ cart | total }}</p> // Component: cart = [{ price: 10 }]; add(item: { price: number }) { this.cart.push(item); }- APipes can't receive arrays, only primitive values
- BThe pipe must implement
OnChangesto notice new items - CPipes are evaluated on the first render, then cached forever
- DA pure pipe re-runs only when its input changes by reference, and
pushmutates the same array
Show answer
Answer: D (A pure pipe re-runs only when its input changes by reference, and
pushmutates the same array)Pipes are pure by default, so Angular skips
transformwhen the input is the same reference (or the same primitive value). Use an immutable update likethis.cart = [...this.cart, item], or a signal pluscomputed. Settingpure: falsealso works, but then the pipe runs on every change detection cycle. - 7.easy
Which part of an
@forblock is mandatory?- AThe
trackexpression - B
let i = $index - CAn
@emptyblock - DA
trackBymethod on the component
Show answer
Answer: A (The
trackexpression)@for (item of items; track item.id) { ... }won't compile withouttrack. It tells Angular how to identify items so it can move and reuse DOM nodes instead of re-creating them. Track a stable id;$indexis only fine for static lists.@emptyis optional. - AThe
- 8.easy
Apart from the constructor, which lifecycle hook runs first for a component whose input is bound in the parent's template?
- A
ngOnInit - B
ngOnChanges - C
ngDoCheck - D
ngAfterViewInit
Show answer
Answer: B (
ngOnChanges)ngOnChangesruns whenever bound inputs are set, including the first time, beforengOnInit. ThenngOnInitruns once, followed byngDoCheck, the content hooks and the view hooks. A component with no bound inputs never getsngOnChanges. - A
- 9.mid
What is logged?
@Component({ selector: 'app-toolbar', template: `@if (show) { <input #search /> }`, }) export class Toolbar { show = true; @ViewChild('search') search?: ElementRef<HTMLInputElement>; ngOnInit() { console.log('init', this.search); } ngAfterViewInit() { console.log('view', this.search); } }- Ainit ElementRef, view ElementRef
- Binit undefined, view undefined
- CAn error:
@ViewChildcan't find elements inside@if - Dinit undefined, view ElementRef
Show answer
Answer: D (init undefined, view ElementRef)
Dynamic view queries (the default
static: false, and the only option that works inside@if/@for) are resolved after the view is created, so the value is first available inngAfterViewInit. The signal-basedviewChild('search')returns a signal instead, which you can read incomputedoreffectwithout worrying about timing. - 10.mid
A Save button sends an HTTP request per click. While a save is in flight, extra clicks should be ignored. Which operator goes in
clicks$.pipe(???(() => this.api.save(form)))?- A
switchMap - B
mergeMap - C
concatMap - D
exhaustMap
Show answer
Answer: D (
exhaustMap)exhaustMapignores new source values while its current inner observable is still active, so double-clicks are dropped.switchMapwould cancel the in-flight save,mergeMapwould send parallel saves, andconcatMapwould queue one save per click. - A
- 11.hard
What does this print?
of(3, 1, 2) .pipe(concatMap((n) => of(n).pipe(delay(n * 100)))) .subscribe(console.log);- A1, 2, 3
- B3, 1, 2
- C3
- D2
Show answer
Answer: B (3, 1, 2)
concatMapsubscribes to the next inner observable only after the previous one completes, so output keeps source order: 3 (after 300 ms), then 1, then 2, even though the later delays are shorter.mergeMapwould print 1, 2, 3, in completion order. - 12.hard
What does this print?
of(3, 1, 2) .pipe(switchMap((n) => of(n).pipe(delay(n * 100)))) .subscribe(console.log);- A3, 1, 2
- B1, 2, 3
- C2
- D3
Show answer
Answer: C (2)
ofemits 3, 1 and 2 synchronously. Each new value makesswitchMapunsubscribe from the previous delayed inner observable before it can emit, so only the last one (2) survives.exhaustMapwould print only 3. - 13.easy
What is logged?
const status = new BehaviorSubject(0); status.next(1); status.subscribe((v) => console.log('A', v)); status.next(2);- AA 0, A 1, A 2
- BA 1, A 2
- CA 2
- DA 0, A 2
Show answer
Answer: B (A 1, A 2)
A
BehaviorSubjectholds the current value and emits it immediately to each new subscriber, so A first receives 1 (the initial 0 had already been replaced) and then 2. With a plainSubject, A would only receive 2. - 14.mid
What is logged?
const subject = new Subject<number>(); const replay = new ReplaySubject<number>(2); [1, 2, 3].forEach((n) => { subject.next(n); replay.next(n); }); subject.subscribe((v) => console.log('subject', v)); replay.subscribe((v) => console.log('replay', v));- Areplay 2, replay 3
- Bsubject 3, replay 2, replay 3
- Creplay 1, replay 2
- Dsubject 1, subject 2, subject 3, replay 2, replay 3
Show answer
Answer: A (replay 2, replay 3)
A
Subjecthas no memory: values emitted before you subscribe are simply lost.ReplaySubject(2)buffers the last two values and replays them to every new subscriber. - 15.easy
How many HTTP requests does this code send?
const users$ = this.http.get<User[]>('/api/users'); users$.subscribe((users) => this.render(users)); users$.subscribe((users) => this.cache(users));- AOne: the second subscriber reuses the response
- BNone until
firstValueFrom()is called - CTwo
- DOne, and the second
subscribethrows
Show answer
Answer: C (Two)
HttpClient observables are cold: every subscription runs the request again. Subscribe once, or share the result with
shareReplay(1). The flip side: if nothing subscribes, no request is sent at all. - 17.mid
What happens when this component initializes?
@Component({ selector: 'app-ticker', template: '' }) export class Ticker implements OnInit { ngOnInit() { interval(1000) .pipe(takeUntilDestroyed()) .subscribe((n) => console.log(n)); } }- AIt ticks until the component is destroyed
- BIt throws NG0203: without an argument,
takeUntilDestroyed()needs an injection context - CIt never ticks, because it completes immediately
- DIt ticks forever, because
takeUntilDestroyedonly works with the async pipe
Show answer
Answer: B (It throws NG0203: without an argument,
takeUntilDestroyed()needs an injection context)With no argument,
takeUntilDestroyed()injects the currentDestroyRef, which only works in an injection context such as the constructor or a field initializer. InngOnInit, injectDestroyRefinto a field and pass it:takeUntilDestroyed(this.destroyRef). - 18.mid
A signed-out user navigates to
/account. What happens?export const authGuard: CanActivateFn = () => { const auth = inject(AuthService); const router = inject(Router); return auth.isLoggedIn() ? true : router.createUrlTree(['/login']); }; // { path: 'account', component: Account, canActivate: [authGuard] }- ANavigation to /account completes and a warning is logged
- BIt throws, because
inject()can't be used in a plain function - CThe /account navigation is cancelled and the router redirects to /login
- DThe page stays blank until the guard runs again
Show answer
Answer: C (The /account navigation is cancelled and the router redirects to /login)
Functional guards run in an injection context, so
inject()works inside them. Returning aUrlTreecancels the current navigation and starts a new one to that URL; returningfalsewould only cancel it. Client-side guards are UX: the server must still enforce authorization. - 19.easy
Inside an
HttpInterceptorFn(req, next) => ..., which snippet correctly adds anAuthorizationheader?- A
req.headers.set('Authorization', token); return next(req); - B
req.setHeader('Authorization', token); return next(req); - C
return next({ ...req, headers: { Authorization: token } }); - D
return next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } }));
Show answer
Answer: D (
return next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } }));)HttpRequestandHttpHeadersare immutable:req.headers.set(...)returns a new headers object and leavesrequnchanged, so passingreqon sends it without the header.req.clone()withsetHeaderscreates a modified copy. Register it withprovideHttpClient(withInterceptors([authInterceptor])). - A
- 20.easy
Where does the
<p>end up?<!-- card.component.html --> <header><ng-content select="[card-title]" /></header> <section><ng-content /></section> <!-- usage --> <app-card> <h2 card-title>Hello</h2> <p>Body text</p> </app-card>- AInside the
section, via the default<ng-content /> - BInside the
header, next to the title - CNowhere: it doesn't match any selector
- DIn both the header and the section
Show answer
Answer: A (Inside the
section, via the default<ng-content />)Content matching
select="[card-title]"goes into that slot; everything else goes into the<ng-content>without a selector. Each node is projected into at most one slot, and without a default slot unmatched content isn't rendered at all. - AInside the
- 21.mid
What is the inferred type of
name.value, and what is logged?const name = new FormControl('Ada'); name.setValue('Grace'); name.reset(); console.log(name.value);- A
string, and it logs 'Ada' - B
string, and it logs '' - C
any, and it logs undefined - D
string | null, and it logs null
Show answer
Answer: D (
string | null, and it logs null)Typed forms infer
FormControl<string | null>becausereset()sets the value tonullby default. Pass{ nonNullable: true }(or usefb.nonNullable) to get astringtype and havereset()restore the initial value 'Ada'. - A
- 22.hard
The app uses
provideZonelessChangeDetection()and nothing else happens on the page. What does the user see two seconds after the component renders?@Component({ selector: 'app-status', template: `<p>{{ status }}</p>`, }) export class Status { status = 'loading'; constructor() { setTimeout(() => (this.status = 'done'), 1000); } }- Adone
- Bloading
- CAn NG0100 error
- Dloading and done flickering
Show answer
Answer: B (loading)
Without zone.js, Angular isn't told that a timer callback ran. Zoneless change detection is scheduled by notifications: a signal read in a template changing, a template event handler,
markForCheck()(which theasyncpipe calls), and similar. A plain field assignment triggers none of them. Makestatusa signal and callthis.status.set('done'). - 23.easy
Which route definition lazy-loads a standalone component?
- A
{ path: 'admin', component: AdminComponent, lazy: true } - B
{ path: 'admin', loadComponent: () => import('./admin.component').then((m) => m.AdminComponent) } - C
{ path: 'admin', loadChildren: AdminComponent } - D
{ path: 'admin', component: () => import('./admin.component') }
Show answer
Answer: B (
{ path: 'admin', loadComponent: () => import('./admin.component').then((m) => m.AdminComponent) })loadComponenttakes a function returning a dynamicimport(), so the bundler puts the component in its own chunk that downloads on first navigation.loadChildrenlazily loads aRoutesarray instead. If the component is the file's default export, the.then(...)can be omitted. - A
- 24.hard
typeTwice()is called from a click handler after the component has rendered. What does the effect log as a result of that call?export class Search { query = signal(''); constructor() { effect(() => console.log('query is', this.query())); } typeTwice() { this.query.set('a'); this.query.set('ab'); } }- A'query is a', then 'query is ab'
- B'query is ab', twice
- CNothing: effects only run once, when they're created
- D'query is ab', once
Show answer
Answer: D ('query is ab', once)
Effects don't run synchronously on every
set. They're marked dirty and executed asynchronously as part of change detection, reading the signals' current values, so two synchronous writes lead to a single run that sees 'ab'. (Every effect also runs once initially, which logged the empty query.) - 25.hard
Given these providers on a component, is
a === bin its child?providers: [ NewLogger, { provide: OldLogger, useExisting: NewLogger }, ], // In a child component: private a = inject(NewLogger); private b = inject(OldLogger);- AYes:
useExistingmakes theOldLoggertoken resolve to the sameNewLoggerinstance - BNo:
useExistingcreates a secondNewLoggerinstance - CNo:
bis anOldLoggerinstance - DIt throws, because
OldLoggerhas no class provider
Show answer
Answer: A (Yes:
useExistingmakes theOldLoggertoken resolve to the sameNewLoggerinstance)useExistingaliases one token to another, so both tokens resolve to one instance.useClass: NewLoggerwould create a separate instance for theOldLoggertoken. Aliasing is handy when migrating consumers from an old service to a new one. - AYes:
- 26.mid
Non-admin users must not even download the admin route's lazy chunk, and should fall through to another route with the same path. Which guard fits?
- A
canActivate - B
canDeactivate - C
canActivateChild - D
canMatch
Show answer
Answer: D (
canMatch)canMatchruns while the router is matching routes. If it returns false, the route is treated as not matching, so itsloadChildren/loadComponentisn't loaded and the router moves on to the next route with that path. It replaced the deprecatedcanLoad. - A
- 27.mid
What does this template do?
@defer (on viewport) { <app-sales-chart /> } @placeholder { <div class="chart-skeleton"></div> }- ARenders the chart immediately but hides it until it's scrolled into view
- BDelays change detection for the chart until it is visible
- CLoads the chart's code as a separate chunk and renders it once the placeholder is in view
- DOnly works when the chart is lazy-loaded through the router
Show answer
Answer: C (Loads the chart's code as a separate chunk and renders it once the placeholder is in view)
@defermoves standalone dependencies used only inside the block into a separate chunk. The placeholder renders first; when it enters the viewport (via IntersectionObserver) Angular fetches the chunk and swaps in the chart. If the chart were also referenced elsewhere in the same file, it would be bundled eagerly. - 28.easy
An OnPush component displays
{{ user$ | async }}, anduser$emits later when an HTTP call returns. Why does the view update?- AOnPush components are checked whenever any observable in the app emits
- BHttpClient triggers a full refresh that ignores OnPush
- CIt doesn't: you have to subscribe manually and call
detectChanges() - DThe
asyncpipe callsmarkForCheck()whenever it receives a new value
Show answer
Answer: D (The
asyncpipe callsmarkForCheck()whenever it receives a new value)The
asyncpipe subscribes for you, marks the component and its ancestors for check on every emission, and unsubscribes when the view is destroyed. That is why it works in OnPush and zoneless apps without manualChangeDetectorRefcalls. - 29.mid
What is logged?
const a$ = new Subject<string>(); const b$ = new Subject<string>(); combineLatest([a$, b$]).subscribe(([a, b]) => console.log(a + b)); a$.next('a1'); a$.next('a2'); b$.next('b1'); a$.next('a3');- Aa1b1, a2b1, a3b1
- Ba3b1
- Ca2b1, a3b1
- Da1b1, a3b1
Show answer
Answer: C (a2b1, a3b1)
combineLatestemits nothing until every source has emitted at least once; after that, any emission produces the latest value from each source.a1was replaced bya2beforeb$emitted, so it's never combined. UsestartWithon a source if you need an immediate first value. - 30.mid
What is logged?
interval(10) .pipe( map((n) => { if (n === 2) throw new Error('boom'); return n; }), catchError(() => of('fallback')), ) .subscribe({ next: console.log, complete: () => console.log('complete') });- A0, 1, fallback, 3, 4, ... (keeps going)
- B0, 1, fallback, complete
- C0, 1, then an uncaught error
- D0, 1, complete
Show answer
Answer: B (0, 1, fallback, complete)
An error terminates the source stream.
catchErrorswaps in the observable you return, hereof('fallback'), which emits once and completes, so the whole chain completes. To keep an outer stream alive (like a search box), putcatchErroron the inner observable insideswitchMap. - 31.mid
A child component declares
value = model(0). In the parent,<app-stepper [(value)]="count" />with a plaincountfield is shorthand for which binding?- A
<app-stepper [value]="count" (valueChange)="count = $event" /> - B
<app-stepper [value]="count" (change)="count = $event" /> - C
<app-stepper [model]="count" /> - D
<app-stepper (value)="count" />
Show answer
Answer: A (
<app-stepper [value]="count" (valueChange)="count = $event" />)model()creates a writable signal input plus an output with the same name and aChangesuffix. When the child callsthis.value.set(...), it emitsvalueChange, and the banana-in-a-box syntax wires both directions. Ifcountis a writable signal, Angular updates the signal for you. - A
- 32.easy
This standalone component fails to compile because the
currencypipe can't be found. What's the fix?@Component({ selector: 'app-price', template: `{{ amount | currency }}`, }) export class Price { amount = 42; }- AAdd
standalone: true - BAdd
imports: [CurrencyPipe]to the component - CDeclare the pipe in
AppModule - DRegister the pipe globally in
app.config.ts
Show answer
Answer: B (Add
imports: [CurrencyPipe]to the component)Standalone components, the default in modern Angular, must list every component, directive and pipe their template uses in their own
importsarray; here that'sCurrencyPipefrom@angular/common. There's no global registration, which keeps dependencies explicit and tree-shakable. - AAdd