#The Angular mental model
An Angular application is a tree of components. Each component is a TypeScript class (the state and the behaviour) paired with a template (the HTML it renders) and a little metadata (its selector, what its template uses, its styles). One component sits at the root; its template uses other components, whose templates use others, and so on down to the leaves. Everything you see on screen is owned by exactly one component in that tree.
Around the tree there are two pieces of machinery you don't write yourself:
- Dependency injection (DI). Components don't
newtheir services. They ask for them withinject(SomeService), and an injector hands over a shared instance. Injectors form their own tree: one for the whole application (filled fromappConfig), plus one per component that declares providers. See The idea behind dependency injection. - Change detection. When your state changes, Angular updates the parts of the DOM that depend on it. You never touch the DOM to show a new value — you change the state, and the template's bindings are re-evaluated. In Angular 22 the trigger is mostly signals: a template that reads
count()is re-rendered whencountis set. See How change detection works.
A good mental model is a spreadsheet. The component's state is the input cells. Every {{ }} and [binding] in the template is a formula cell that reads them. You never type into a formula cell; you change an input cell and the spreadsheet recalculates the formulas that depend on it. Angular's job is to be the recalculation engine — and, because templates are compiled ahead of time, it knows exactly which DOM nodes each formula writes to.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
`,
})
export class Counter {
protected readonly count = signal(0);
increment() {
this.count.update((c) => c + 1);
}
}@Componenttells the Angular compiler "this class is a component". At build time the compiler reads the metadata, parses the template and turns it into JavaScript instructions stored on the class (see The @Component decorator). Nothing is parsed in the browser.selector: 'app-counter'means: wherever another template contains<app-counter>, create aCounterthere. That element becomes the component's host element.count = signal(0)is the state — an input cell. It'sprotectedso the template can read it but code outside the class can't.{{ count() }}is a formula: the template calls the signal to read it, and by reading it the view subscribes to it.(click)="increment()"wires a DOM event to a method.- On a click,
increment()runsupdate(c => c + 1):countgoes0 → 1. Writing to a signal that a template read marks that view dirty and schedules a render. No DOM code anywhere.
Here is the spec that proves it — and shows when each part happens:
const fixture = TestBed.createComponent(Counter);
const host: HTMLElement = fixture.nativeElement;
console.log(host.innerHTML); // <p></p><button>+1</button>
await fixture.whenStable();
console.log(host.querySelector('p')!.textContent); // Count: 0
host.querySelector('button')!.click();
console.log(host.querySelector('p')!.textContent); // Count: 0 (not yet!)
await fixture.whenStable();
console.log(host.querySelector('p')!.textContent); // Count: 1TestBed.createComponent(Counter)instantiates the class and runs the template's creation pass: it creates the<p>, an empty text node inside it, and the<button>, and attaches the click listener. The bindings haven't run yet, so the<p>is empty — the real output was"<p></p><button>+1</button>".await fixture.whenStable()lets Angular run its first update pass: it evaluatescount()and writesCount: 0into the text node.button.click()runsincrement()synchronously, socountis already1. But the DOM still saysCount: 0: in zoneless Angular 22, the write only schedules change detection.- The second
whenStable()lets the scheduled render run, and the text becomesCount: 1. That's the whole loop — event → state change → scheduled re-render — and you'll see it in every chapter.
What the pieces are, in one line each:
| Piece | What it is | Chapter |
|---|---|---|
| Component | class + template + metadata; one per host element | Components |
| Template binding | {{ }}, [prop], (event), [(two-way)] | Interpolation |
| Directive | behaviour added to an existing element | What directives are |
| Pipe | a value transform used in templates | What pipes are |
| Service + DI | shared logic and state, handed out by injectors | Dependency Injection |
| Signal | reactive state that templates and computed() track | Signals |
| Router | maps URLs to components | Routing |
Angular vs a library like React, in the terms interviewers use: Angular is a framework — it owns the app's structure, ships its own router, forms, HTTP client, DI container and test setup, and compiles templates ahead of time. React is a view library you assemble those things around. The trade-off is less choice for more consistency: two Angular codebases look much more alike than two React ones.