pencils ready ✎

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. 1.

    The parent's Rename button is clicked. What does the OnPush card show afterwards?

    mid
    @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'; }
    1. AGrace: the click ran change detection for the whole app
    2. BStill Ada: the input reference didn't change, so the OnPush card is never marked for check
    3. CAngular throws ExpressionChangedAfterItHasBeenCheckedError
    4. 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.name keeps 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 use update.

  2. 2.

    What is logged?

    mid
    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());
    1. Acomputing, 6, 6
    2. Bcomputing, computing, computing, 6, 6
    3. Ccomputing, 6, computing, 6
    4. D6, 6
    Show answer

    Answer: A (computing, 6, 6)

    computed is lazy: nothing runs until it's first read, so the two set calls don't compute anything. The first read computes and caches 6; the second read returns the cached value because count hasn't changed since.

  3. 3.

    What is logged?

    hard
    const user = signal({ name: 'Ada' });
    const greeting = computed(() => `Hi ${user().name}`);
    
    console.log(greeting());
    user().name = 'Grace';
    console.log(greeting());
    1. AHi Ada, Hi Grace
    2. BHi Ada, then an error because signal values are frozen
    3. CHi Ada, Hi Ada
    4. DHi Grace, Hi Grace
    Show answer

    Answer: C (Hi Ada, Hi Ada)

    Mutating the object held by a signal doesn't notify anything, so greeting keeps returning its memoized value. Produce a new object instead: user.update(u => ({ ...u, name: 'Grace' })). Signals compare values with Object.is by default.

  4. 4.

    Where in a component class can you call inject(HttpClient) without any extra setup?

    mid
    1. AInside ngOnInit
    2. BInside a setTimeout callback started from the constructor
    3. CIn a field initializer or the constructor
    4. 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 in runInInjectionContext. Calling it later, e.g. in ngOnInit or a callback, throws NG0203. Inject into a field (private http = inject(HttpClient)) and use the field anywhere.

  5. 5.

    CartService is @Injectable({ providedIn: 'root' }), and ProductPanel also declares providers: [CartService]. The page shows two ProductPanels, and the header (outside the panels) injects CartService too. How many instances exist?

    hard
    1. A1
    2. B2
    3. C3
    4. DNone: providing a root service again is a compile error
    Show answer

    Answer: C (3)

    Each ProductPanel instance gets its own element injector with its own CartService, 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. 6.

    After add() runs, the displayed total doesn't change. Why?

    mid
    @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); }
    1. APipes can't receive arrays, only primitive values
    2. BThe pipe must implement OnChanges to notice new items
    3. CPipes are evaluated on the first render, then cached forever
    4. DA pure pipe re-runs only when its input changes by reference, and push mutates the same array
    Show answer

    Answer: D (A pure pipe re-runs only when its input changes by reference, and push mutates the same array)

    Pipes are pure by default, so Angular skips transform when the input is the same reference (or the same primitive value). Use an immutable update like this.cart = [...this.cart, item], or a signal plus computed. Setting pure: false also works, but then the pipe runs on every change detection cycle.

  7. 7.

    Which part of an @for block is mandatory?

    easy
    1. AThe track expression
    2. Blet i = $index
    3. CAn @empty block
    4. DA trackBy method on the component
    Show answer

    Answer: A (The track expression)

    @for (item of items; track item.id) { ... } won't compile without track. It tells Angular how to identify items so it can move and reuse DOM nodes instead of re-creating them. Track a stable id; $index is only fine for static lists. @empty is optional.

  8. 8.

    Apart from the constructor, which lifecycle hook runs first for a component whose input is bound in the parent's template?

    easy
    1. AngOnInit
    2. BngOnChanges
    3. CngDoCheck
    4. DngAfterViewInit
    Show answer

    Answer: B (ngOnChanges)

    ngOnChanges runs whenever bound inputs are set, including the first time, before ngOnInit. Then ngOnInit runs once, followed by ngDoCheck, the content hooks and the view hooks. A component with no bound inputs never gets ngOnChanges.

  9. 9.

    What is logged?

    mid
    @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); }
    }
    1. Ainit ElementRef, view ElementRef
    2. Binit undefined, view undefined
    3. CAn error: @ViewChild can't find elements inside @if
    4. 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 in ngAfterViewInit. The signal-based viewChild('search') returns a signal instead, which you can read in computed or effect without worrying about timing.

  10. 10.

    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)))?

    mid
    1. AswitchMap
    2. BmergeMap
    3. CconcatMap
    4. DexhaustMap
    Show answer

    Answer: D (exhaustMap)

    exhaustMap ignores new source values while its current inner observable is still active, so double-clicks are dropped. switchMap would cancel the in-flight save, mergeMap would send parallel saves, and concatMap would queue one save per click.

  11. 11.

    What does this print?

    hard
    of(3, 1, 2)
      .pipe(concatMap((n) => of(n).pipe(delay(n * 100))))
      .subscribe(console.log);
    1. A1, 2, 3
    2. B3, 1, 2
    3. C3
    4. D2
    Show answer

    Answer: B (3, 1, 2)

    concatMap subscribes 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. mergeMap would print 1, 2, 3, in completion order.

  12. 12.

    What does this print?

    hard
    of(3, 1, 2)
      .pipe(switchMap((n) => of(n).pipe(delay(n * 100))))
      .subscribe(console.log);
    1. A3, 1, 2
    2. B1, 2, 3
    3. C2
    4. D3
    Show answer

    Answer: C (2)

    of emits 3, 1 and 2 synchronously. Each new value makes switchMap unsubscribe from the previous delayed inner observable before it can emit, so only the last one (2) survives. exhaustMap would print only 3.

  13. 13.

    What is logged?

    easy
    const status = new BehaviorSubject(0);
    status.next(1);
    status.subscribe((v) => console.log('A', v));
    status.next(2);
    1. AA 0, A 1, A 2
    2. BA 1, A 2
    3. CA 2
    4. DA 0, A 2
    Show answer

    Answer: B (A 1, A 2)

    A BehaviorSubject holds 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 plain Subject, A would only receive 2.

  14. 14.

    What is logged?

    mid
    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));
    1. Areplay 2, replay 3
    2. Bsubject 3, replay 2, replay 3
    3. Creplay 1, replay 2
    4. Dsubject 1, subject 2, subject 3, replay 2, replay 3
    Show answer

    Answer: A (replay 2, replay 3)

    A Subject has 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. 15.

    How many HTTP requests does this code send?

    easy
    const users$ = this.http.get<User[]>('/api/users');
    
    users$.subscribe((users) => this.render(users));
    users$.subscribe((users) => this.cache(users));
    1. AOne: the second subscriber reuses the response
    2. BNone until firstValueFrom() is called
    3. CTwo
    4. DOne, and the second subscribe throws
    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.

  16. 16.

    Three components call getConfig() and subscribe at different times, all after the first response has arrived. How many requests reach the server?

    mid
    @Injectable({ providedIn: 'root' })
    export class ConfigService {
      private http = inject(HttpClient);
      private config$ = this.http
        .get<Config>('/api/config')
        .pipe(shareReplay(1));
    
      getConfig() {
        return this.config$;
      }
    }
    1. A3
    2. B2
    3. C0
    4. D1
    Show answer

    Answer: D (1)

    shareReplay(1) shares one subscription to the source and replays the last value to later subscribers, so all three get the cached config from a single request. Plain share() would re-run the request for a subscriber that arrives after the source has completed.

  17. 17.

    What happens when this component initializes?

    mid
    @Component({ selector: 'app-ticker', template: '' })
    export class Ticker implements OnInit {
      ngOnInit() {
        interval(1000)
          .pipe(takeUntilDestroyed())
          .subscribe((n) => console.log(n));
      }
    }
    1. AIt ticks until the component is destroyed
    2. BIt throws NG0203: without an argument, takeUntilDestroyed() needs an injection context
    3. CIt never ticks, because it completes immediately
    4. DIt ticks forever, because takeUntilDestroyed only 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 current DestroyRef, which only works in an injection context such as the constructor or a field initializer. In ngOnInit, inject DestroyRef into a field and pass it: takeUntilDestroyed(this.destroyRef).

  18. 18.

    A signed-out user navigates to /account. What happens?

    mid
    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] }
    1. ANavigation to /account completes and a warning is logged
    2. BIt throws, because inject() can't be used in a plain function
    3. CThe /account navigation is cancelled and the router redirects to /login
    4. 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 a UrlTree cancels the current navigation and starts a new one to that URL; returning false would only cancel it. Client-side guards are UX: the server must still enforce authorization.

  19. 19.

    Inside an HttpInterceptorFn (req, next) => ..., which snippet correctly adds an Authorization header?

    easy
    1. Areq.headers.set('Authorization', token); return next(req);
    2. Breq.setHeader('Authorization', token); return next(req);
    3. Creturn next({ ...req, headers: { Authorization: token } });
    4. Dreturn next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } }));
    Show answer

    Answer: D (return next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } }));)

    HttpRequest and HttpHeaders are immutable: req.headers.set(...) returns a new headers object and leaves req unchanged, so passing req on sends it without the header. req.clone() with setHeaders creates a modified copy. Register it with provideHttpClient(withInterceptors([authInterceptor])).

  20. 20.

    Where does the <p> end up?

    easy
    <!-- 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>
    1. AInside the section, via the default <ng-content />
    2. BInside the header, next to the title
    3. CNowhere: it doesn't match any selector
    4. 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.

  21. 21.

    What is the inferred type of name.value, and what is logged?

    mid
    const name = new FormControl('Ada');
    name.setValue('Grace');
    name.reset();
    console.log(name.value);
    1. Astring, and it logs 'Ada'
    2. Bstring, and it logs ''
    3. Cany, and it logs undefined
    4. Dstring | null, and it logs null
    Show answer

    Answer: D (string | null, and it logs null)

    Typed forms infer FormControl<string | null> because reset() sets the value to null by default. Pass { nonNullable: true } (or use fb.nonNullable) to get a string type and have reset() restore the initial value 'Ada'.

  22. 22.

    The app uses provideZonelessChangeDetection() and nothing else happens on the page. What does the user see two seconds after the component renders?

    hard
    @Component({
      selector: 'app-status',
      template: `<p>{{ status }}</p>`,
    })
    export class Status {
      status = 'loading';
    
      constructor() {
        setTimeout(() => (this.status = 'done'), 1000);
      }
    }
    1. Adone
    2. Bloading
    3. CAn NG0100 error
    4. 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 the async pipe calls), and similar. A plain field assignment triggers none of them. Make status a signal and call this.status.set('done').

  23. 23.

    Which route definition lazy-loads a standalone component?

    easy
    1. A{ path: 'admin', component: AdminComponent, lazy: true }
    2. B{ path: 'admin', loadComponent: () => import('./admin.component').then((m) => m.AdminComponent) }
    3. C{ path: 'admin', loadChildren: AdminComponent }
    4. D{ path: 'admin', component: () => import('./admin.component') }
    Show answer

    Answer: B ({ path: 'admin', loadComponent: () => import('./admin.component').then((m) => m.AdminComponent) })

    loadComponent takes a function returning a dynamic import(), so the bundler puts the component in its own chunk that downloads on first navigation. loadChildren lazily loads a Routes array instead. If the component is the file's default export, the .then(...) can be omitted.

  24. 24.

    typeTwice() is called from a click handler after the component has rendered. What does the effect log as a result of that call?

    hard
    export class Search {
      query = signal('');
    
      constructor() {
        effect(() => console.log('query is', this.query()));
      }
    
      typeTwice() {
        this.query.set('a');
        this.query.set('ab');
      }
    }
    1. A'query is a', then 'query is ab'
    2. B'query is ab', twice
    3. CNothing: effects only run once, when they're created
    4. 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. 25.

    Given these providers on a component, is a === b in its child?

    hard
    providers: [
      NewLogger,
      { provide: OldLogger, useExisting: NewLogger },
    ],
    
    // In a child component:
    private a = inject(NewLogger);
    private b = inject(OldLogger);
    1. AYes: useExisting makes the OldLogger token resolve to the same NewLogger instance
    2. BNo: useExisting creates a second NewLogger instance
    3. CNo: b is an OldLogger instance
    4. DIt throws, because OldLogger has no class provider
    Show answer

    Answer: A (Yes: useExisting makes the OldLogger token resolve to the same NewLogger instance)

    useExisting aliases one token to another, so both tokens resolve to one instance. useClass: NewLogger would create a separate instance for the OldLogger token. Aliasing is handy when migrating consumers from an old service to a new one.

  26. 26.

    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?

    mid
    1. AcanActivate
    2. BcanDeactivate
    3. CcanActivateChild
    4. DcanMatch
    Show answer

    Answer: D (canMatch)

    canMatch runs while the router is matching routes. If it returns false, the route is treated as not matching, so its loadChildren/loadComponent isn't loaded and the router moves on to the next route with that path. It replaced the deprecated canLoad.

  27. 27.

    What does this template do?

    mid
    @defer (on viewport) {
      <app-sales-chart />
    } @placeholder {
      <div class="chart-skeleton"></div>
    }
    1. ARenders the chart immediately but hides it until it's scrolled into view
    2. BDelays change detection for the chart until it is visible
    3. CLoads the chart's code as a separate chunk and renders it once the placeholder is in view
    4. 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)

    @defer moves 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. 28.

    An OnPush component displays {{ user$ | async }}, and user$ emits later when an HTTP call returns. Why does the view update?

    easy
    1. AOnPush components are checked whenever any observable in the app emits
    2. BHttpClient triggers a full refresh that ignores OnPush
    3. CIt doesn't: you have to subscribe manually and call detectChanges()
    4. DThe async pipe calls markForCheck() whenever it receives a new value
    Show answer

    Answer: D (The async pipe calls markForCheck() whenever it receives a new value)

    The async pipe 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 manual ChangeDetectorRef calls.

  29. 29.

    What is logged?

    mid
    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');
    1. Aa1b1, a2b1, a3b1
    2. Ba3b1
    3. Ca2b1, a3b1
    4. Da1b1, a3b1
    Show answer

    Answer: C (a2b1, a3b1)

    combineLatest emits nothing until every source has emitted at least once; after that, any emission produces the latest value from each source. a1 was replaced by a2 before b$ emitted, so it's never combined. Use startWith on a source if you need an immediate first value.

  30. 30.

    What is logged?

    mid
    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') });
    1. A0, 1, fallback, 3, 4, ... (keeps going)
    2. B0, 1, fallback, complete
    3. C0, 1, then an uncaught error
    4. D0, 1, complete
    Show answer

    Answer: B (0, 1, fallback, complete)

    An error terminates the source stream. catchError swaps in the observable you return, here of('fallback'), which emits once and completes, so the whole chain completes. To keep an outer stream alive (like a search box), put catchError on the inner observable inside switchMap.

  31. 31.

    A child component declares value = model(0). In the parent, <app-stepper [(value)]="count" /> with a plain count field is shorthand for which binding?

    mid
    1. A<app-stepper [value]="count" (valueChange)="count = $event" />
    2. B<app-stepper [value]="count" (change)="count = $event" />
    3. C<app-stepper [model]="count" />
    4. 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 a Change suffix. When the child calls this.value.set(...), it emits valueChange, and the banana-in-a-box syntax wires both directions. If count is a writable signal, Angular updates the signal for you.

  32. 32.

    This standalone component fails to compile because the currency pipe can't be found. What's the fix?

    easy
    @Component({
      selector: 'app-price',
      template: `{{ amount | currency }}`,
    })
    export class Price {
      amount = 42;
    }
    1. AAdd standalone: true
    2. BAdd imports: [CurrencyPipe] to the component
    3. CDeclare the pipe in AppModule
    4. 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 imports array; here that's CurrencyPipe from @angular/common. There's no global registration, which keeps dependencies explicit and tree-shakable.

esc