Lesson 30 of 30
This track's closing lesson: a real, slightly dated Angular component gets run through every idea covered so far — standalone status, signal usage, change detection, control flow, and DI scope — and fixed.
18 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
An interview rarely asks "what does input() do" in isolation — it hands you a component and asks "what would you change here, and why." That's a synthesis question, and answering it well means running one real component through everything this track covered, not reciting facts about signals and DI separately. This lesson is that pass, applied to one plausible, slightly-dated UserCardComponent that a real codebase migrating off legacy Angular might still have lying around.
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-card',
imports: [CommonModule],
template: `
<div *ngIf="user">
<h3>{{ user.name }}</h3>
<ul>
<li *ngFor="let tag of user.tags">{{ tag }}</li>
</ul>
<button (click)="onSelect.emit(user.id)">Select</button>
</div>
`,
})
export class UserCardComponent implements OnInit {
@Input() user!: { id: string; name: string; tags: string[] };
@Output() onSelect = new EventEmitter<string>();
ngOnInit() {
console.log('user card mounted');
}
}
Nothing here is broken — it compiles and runs. But run it through the checklist:
*ngIf and *ngFor still work, but @if and @for are compiler-native, don't need CommonModule imported at all, and @for would force an explicit track — which this component is quietly missing entirely, since *ngFor doesn't require one.@Input() user! uses the definite-assignment assertion to paper over the fact that TypeScript can't verify it'll actually be set — input.required<User>() gives you that guarantee for real, enforced at the type level, no ! needed.onSelect is a naming trap: Angular's own style convention avoids prefixing outputs with on, both because it invites confusion with native DOM event names and because the newer output() API's callers read more naturally as (select)="..." than (onSelect)="...".OnPush, rather than leaving it to the default.providers: [...] array purely so this component gets a "fresh" instance, that's easy to miss and easy to accidentally duplicate — a real audit always asks where an injected dependency is provided, not just whether inject() was used correctly.import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
interface User {
id: string;
name: string;
tags: string[];
}
@Component({
selector: 'app-user-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (user(); as u) {
<div>
<h3>{{ u.name }}</h3>
<ul>
@for (tag of u.tags; track tag) {
<li>{{ tag }}</li>
}
</ul>
<button (click)="select.emit(u.id)">Select</button>
</div>
}
`,
})
export class UserCardComponent {
user = input.required<User>();
select = output<string>();
}
No CommonModule import (the control-flow blocks need none), no ! type-lying, no lifecycle hook doing nothing but logging, and a track expression the compiler now enforces at build time instead of leaving as a silent runtime performance gap.
*ngIf/*ngFor legacy syntax and call the audit done — this component still had a naming violation, a missing OnPush, and a DI-scope question that a quick glance at the template alone would never surface.@Input() x!: T as equivalent to input.required<T>(). The definite-assignment assertion (!) is a promise to the compiler with no runtime enforcement; input.required() is a real contract Angular itself enforces — if the parent never binds it, you get an actual, loud error, not a silent undefined at runtime.OnPush is only a performance micro-optimization, so it's fine to skip. In a zoneless-leaning Angular app, being explicit about change-detection strategy is closer to a correctness statement about how this component expects to be notified of changes — not an afterthought you bolt on later.providedIn: 'root' vs. locally provided, or whether its parent is passing stale non-signal data into a signal input, requires looking one level up from the file you're editing.This is the same synthesis exercise this platform runs at the close of every track — the payoff of the standalone-components lesson, the signals lesson, the control-flow lesson, the signal-inputs-and-outputs lesson, and the dependency-injection lessons isn't remembering each one in isolation, it's being able to run all of them over one real diff and name specific, checkable problems instead of a vague "this looks a bit old." That's the same shape of question interviewers actually ask, and the same shape of work a real migration off legacy Angular patterns looks like in practice.