DevelopmentSenior

Angular

Angular is Google’s TypeScript framework that supplies the whole front-end stack and upgrades your code between major versions with automated migrations. The stack means components, dependency injection, routing, forms, HTTP, testing and the build, chosen and released together. Between 2022 and 2026 it replaced most of the way it used to be written: standalone components instead of NgModules, signals instead of zone.js, and OnPush as the default. The page opens with every building block on one screen, the current way beside the one it replaced, then covers releases and support, dependency injection, change detection, signals and RxJS, forms and security, and five documented cases.

Current version
22.2 · September 2026
Language
TypeScript 6.0+, required
Major releases
One a year, since v22

Angular on one screen: every building block, then and now

An Angular application is built from a small number of kinds of thing: components, the services they depend on, templates, inputs and outputs, and the machinery that decides when the screen is updated. Almost every one of them now has a current way and an older way that still works — and most of the material online teaches the older one. Here is all of it first. The table lists each job, the way Angular 22 does it (September 2026), the way it replaced, and the version in which the current way became stable. It is also the fastest way to tell which Angular a tutorial, a code base or a job candidate is describing.

JobThe current wayWhat it replacedSince
Organising codeStandalone components that list their own importsNgModules declaring groups of componentsv15 · default v19
Getting a dependencyinject() in a field or a functionConstructor parameters — still supportedv14
Component statesignal()Plain fields, re-checked by zone.js after every eventv17
Derived statecomputed()Getters, or values recalculated in ngOnChangesv17
State that follows an input but can be editedlinkedSignal()Copying an input into a field in ngOnChangesv20
Reacting to state outside the templateeffect() — sparinglyngOnChanges, or a subscribe in ngOnInitv20
Inputsinput(), input.required()@Input() decoratorsv19
Outputsoutput()@Output() with an EventEmitterv19
Two-way bindingmodel()An @Input() plus an @Output() whose name ends in Changev19
Reaching child elementsviewChild(), contentChild()@ViewChild, @ContentChildv19
Conditions and loops in templates@if, @for (…; track …), @switch*ngIf, *ngFor, *ngSwitch — deprecated in v20v18
A local variable in a template@letThe *ngIf="data as d" trickv19
Loading part of a page later@defer blocksA lazy route, or a hand-written dynamic importv18
Loading dataresource(), httpResource()HttpClient plus subscribe plus loading and error flags kept by handv22
FormsSignal Forms, form()Reactive Forms and template-driven forms — neither deprecatedv22
What starts a checkZoneless: a signal change, a template event, markForCheckzone.js patching every async browser APIv20.2 · default v21
What gets checkedOnPush, by defaultThe Default strategy, now named Eagerdefault v22
HTTP transportfetchXMLHttpRequest — still there as withXhr() for upload progressdefault v22
Server renderingIncremental hydration: @defer (hydrate on …)Hydrating the whole page at once — or, in older versions, rebuilding itv20 · default v22
Unit testsVitestKarma and Jasminedefault v21
Ending a subscriptiontakeUntilDestroyed(), or the async pipeA Subject completed in ngOnDestroyv19

Everything decided, and upgraded for you

Starting a React project means choosing a router, a data layer, a form library, a styling approach and a build setup before writing a feature. Starting an Angular project means running one command and getting all of them, chosen, integrated and released together. Whether that sounds like relief or like a cage is the honest test of whether the framework suits you. The consequence that matters over years rather than weeks is uniformity: two Angular code bases written by different teams look substantially alike, so an engineer moving between them is productive quickly.

The other consequence is unusual for a framework: Angular takes responsibility for moving your code forward. ng update runs migrations — scripts that rewrite your source for the new version. When v21 stopped using zone.js by default, the migration added provideZoneChangeDetection() to existing applications so they kept working as before. When v22 made every component without an explicit strategy OnPush, the migration added ChangeDetectionStrategy.Eager to exactly those components, so an upgrade did not silently change how any of them behaved. The new default applies to new code; the old behaviour is kept, written down, where it was already relied on.

Between 2022 and 2026 each of Angular’s biggest changes arrived as a new default on a fixed schedule: standalone components stable in v15 and the default in v19, signals and the @if / @for control flow in v17, zoneless change detection by default in v21 and OnPush by default in v22. Only three versions are patched at any time. On 30 September 2026 v22 is active until June 2027 and receives security fixes until June 2028, v21 is in long-term support until June 2027 and v20 until 28 November 2026; everything from v2 to v19 receives nothing. Until v22 a major version came every six months; from v22, once a year.

Migrations only help inside the support window, and the window is short. Each major version is “active” for a while — it gets fixes and small features — and then enters , when only critical fixes and security patches are released. Everything older gets nothing. Until v22 a major version came every six months; from v22 it comes once a year, with four to six minor releases in between, which makes each window longer and each upgrade larger.

VersionReleasedActive untilSecurity fixes until
v223 Jun 2026Jun 2027Jun 2028
v2119 Nov 20253 Jun 2026Jun 2027
v2028 May 202519 Nov 202528 Nov 2026
v2 – v192016 – 2024—None

Angular

  • Router, forms, , testing supplied and integrated
  • and dependency injection are not optional
  • Automated migrations across major versions
  • A large concept load up front, uniform afterwards
  • Templates compiled ahead of time

React

  • A component model; the rest is your choice or a framework’s
  • TypeScript optional, dependency injection unusual
  • Upgrades coordinated across independent packages
  • A small core; complexity accumulates in the choices
  • — the view is ordinary JavaScript

The choice is rarely technical. Angular suits an organisation with many teams, long-lived products and turnover, where one answer per question is worth more than the best answer per question. React suits a team that wants to assemble its own stack and can carry the cost of maintaining that assembly. Both build the same applications; what differs is who makes the decisions and who lives with them.

Dependency injection: where you provide it decides how many exist

A component does not construct the things it needs. It asks for them — private http = inject(HttpClient) — and the framework hands it an instance, without the component knowing where the instance came from or who else is using it. The object that keeps instances and hands them out is called an injector, and Angular has a tree of them.

The payoff is substitution. A test replaces the real HTTP client with a fake by changing one provider, not by patching an import. A component that logs through an injected logger can be given a different logger in a different environment. The cost is indirection: reading a component tells you what it depends on, but not which implementation will arrive. That trade is worth it when the substitution is used, and pure overhead when it is not — a service injected in one place and never replaced has bought nothing and made the code one hop harder to read.

When a component asks for a service, Angular looks in the component’s own injector, then up the component tree, then in the root injector, and the first provider it finds wins. A service provided in root exists once for the whole application and lives until the tab is closed, so all three cards receive the same CartService. A service listed in a component’s providers is created once per instance of that component and destroyed with it, so each card has its own CardState. When nothing provides a service anywhere, the lookup fails with NG0201.

When a component asks for something, Angular looks first in the component’s own injector, then in its parent’s, up the tree of components, and then in the application’s root injector. The first one that has a provider wins. If none has, the request fails with NG0201: No provider for … found — the error every Angular developer meets in the first week, usually because a service was never registered anywhere. Where you register a service therefore decides how many instances exist and how long each one lives:

Provided inHow manyLives untilRight for
providedIn: "root"One for the whole applicationThe tab is closedStateless services, the API client, the signed-in user, caches
A component’s providersOne per instance of that componentThat instance is destroyedThe state of one widget, one form, one step-by-step wizard

How the framework finds work to do

Angular has to decide when to compare each component’s template with its data and update the screen. That comparison is called change detection. For most of Angular’s life the answer to “when” came from zone.js, a library that patches every asynchronous browser — timers, event listeners, network requests — so that Angular is told whenever anything at all finishes. On that signal, Angular walked the component tree and re-evaluated the template expressions.

The same event arrives in the same component in both trees. Under the eager strategy, called Default until v22, Angular checks every component in the application on every turn. Under OnPush it checks only the path from the root down to the component where the change happened, and skips the rest — so the work stops depending on the size of the app and starts depending on where the change is. Since v22 a component that names no strategy is OnPush.

That is less wasteful than it sounds. Angular compiles templates ahead of time, so checking an expression is a property comparison, not an interpretation, and thousands of them are fast. It turns into a problem at scale and in particular shapes: a large tree, a template calling a function that runs on every check, or an event that fires continuously, like scrolling or a mouse moving over a chart.

The first answer was OnPush, and the diagram shows what it buys. A component marked OnPush is checked only when one of its inputs receives a new reference, when an event bound in its own template fires, when a signal its template reads changes, or when something calls markForCheck — which the async pipe does for you. Everything else is skipped, along with its subtree. The cost stops depending on the size of the application and starts depending on where the change is — provided inputs are replaced rather than changed in place, which is why OnPush and immutable updates always arrive together. Since v22, a new component is OnPush unless it says otherwise; the old behaviour, which checks everything, is now called Eager.

Zoneless: knowing instead of guessing

zone.js answers the question “did anything finish?”, and the honest answer is almost always yes: a timer in an analytics script, a mouse move handled by a chart library, a polling request that changed nothing. Each one started a check. Signals let Angular ask a better question — “did anything the templates read change?” — because a signal knows who read it. Zoneless Angular, stable since v20.2 and the default for new applications since v21, drops zone.js altogether and starts a check only for a signal change, an event bound in a template, or an explicit markForCheck.

zone.js patches every asynchronous browser API, so Angular hears about every event that finishes and starts a change-detection pass after each: nine in this illustrative second, seven of which change nothing on screen. Zoneless Angular, the default for new applications since v21, starts a pass only when a signal the templates read changes, when an event bound in a template fires, or when something calls markForCheck — two here. The timings are illustrative; the counts are the point.

The practical effect is less JavaScript shipped, no patched browser APIs, and strictly less work per interaction. The one thing that breaks is code that relied on zone.js noticing a change nobody announced — a plain field assigned inside a setTimeout in an OnPush or zoneless component. The fix is the same in every case: hold that value in a signal.

Signals for state, RxJS for events over time

A signal is a value that records who read it, much like a ref in Vue. signal() holds a value you set; computed() derives one from other signals, lazily and only when they change; linkedSignal() follows a source but can also be set by hand — a selected item that resets when the list changes. Inputs are signals too: name = input<string>() is read as this.name(), so anything derived from inputs is a computed, and most of what ngOnChanges used to do disappears.

effect() is the one to be careful with, and Angular’s guidance matches React’s on useEffect: it is for synchronising with something outside the signal graph — local storage, a chart library, a log — not for keeping one piece of state in step with another. An effect that sets a signal is a computed written the long way, with a moment in between when the two disagree.

Loading data got its own signal API, stable since v22. httpResource(() => "/api/orders/" + this.id()) returns an object whose value, status and error are signals, with six statuses — idle, loading, reloading, resolved, error and local, the last meaning the value was set by hand. When the id changes while a request is still in flight, the resource aborts it. That removes, by construction, the race where a slow old answer overwrites a fast new one — the race the React and Vue pages each solve by hand.

Where RxJS still earns its place

RxJS models a value that arrives many times — a stream — and gives you operators to transform, combine, delay and cancel streams. Angular’s router events and form value changes are streams, so you will meet it whether or not you go looking. Where it earns its complexity is coordination over time: a search box that waits for typing to pause, cancels the previous request on each new keystroke, ignores answers that arrive out of order and retries on failure is four operators and about six lines. Written by hand it is thirty lines with a bug in the cancellation.

Most of that power lives in one decision: what to do with a request that is already running when a new value arrives. RxJS has four operators for it, and choosing the wrong one produces a bug that looks nothing like its cause.

OperatorThe running requestUse it forIn the wrong place
switchMapCancelled; only the latest countsSearch as you type, a route parameterOn saves: a cancelled save is a lost write
mergeMapKept; all run in parallel, answers in any orderIndependent uploads, loggingOn search: an old answer can land last
concatMapKept; the new one waits its turnSaves that must reach the server in orderOn search: a queue of answers nobody wants
exhaustMapKept; the new value is ignoredA pay or submit button pressed twiceOn search: typing is ignored while a request runs

Where RxJS hurts is a value that exists one at a time. A BehaviorSubject holding the current user is a stream with one item, wrapped in machinery designed for many, and every consumer has to subscribe, unsubscribe and handle the initial state. Signals fit that shape better, and the two are meant to live side by side: signals for state, RxJS for events over time, with toSignal() and toObservable() to convert between them.

Forms, and the safety you get by default

Forms are Angular’s strongest ground, and since v22 there are three ways to build them. None is deprecated, and they differ in where the form’s model lives:

ApproachWhere the model livesRight for
Template-drivenIn the template, bound with ngModelA login form, a search box
Reactive FormsIn the class, as FormGroup and FormControl objects, typed since v14Existing applications, forms built at run time
Signal FormsIn a signal holding the data, with validation rules in a schemaNew code on v22 — experimental in v21, public API since v22

Security is the part you get without asking, and it is worth knowing exactly what it covers. Angular treats every value bound in a template as untrusted. Text in {{ }} is escaped, so a comment containing <script> shows up as text. A value bound to [innerHTML] is sanitised — scripts and dangerous attributes are stripped — and URLs in href and src are checked. Templates are compiled when the application is built, so in production there is no template compiler running in the browser that user input could reach. And HttpClient sends the anti-forgery token — read from the XSRF-TOKEN , sent as the X-XSRF-TOKEN header — on requests that change data, only to your own origin, which leaves the server one thing to do: check it.

Five cases, and what each one was missing

Each case below is documented in Angular’s own , guides or security advisories. Together they explain the shape of the framework today — why it invests so heavily in migrations, why templates are compiled before they ship, and why the version you run matters as much as the code you write.

CaseWhat happenedWhat was missing
AngularJS to Angular, 2014–2021Angular 2 was a new framework with the old name, announced without an upgrade pathA migration path — the reason ng update exists
The expression sandbox, 2016AngularJS removed a sandbox that teams had mistaken for a security boundaryThe rule that users never write templates — compiled templates enforce it now
NgModules, 2016–2024Added five weeks before 2.0, they were the hardest concept for newcomers for eight yearsA compiler that could read each component on its own
View Engine to Ivy, 2020–2023The rendering engine was replaced; libraries built for the old one stopped working in v16Dependencies that moved as fast as the framework
CVE-2025-66035, Nov 2025HttpClient sent the anti-forgery token to other hosts; the fix shipped only for supported versionsBeing inside the support window

The first case is the reason the second section of this page exists. AngularJS, released in 2010, was one of the most popular front-end frameworks in the world when Google announced its successor at the ng-europe conference in Paris in October 2014. Angular 2 kept the name and very little else — a new component model, a new template syntax and, from 2015, TypeScript — and at the announcement it had no upgrade path at all. The reaction was fierce, and the team spent the next two years building one: ngUpgrade, which lets both frameworks run in one application while it is moved over piece by piece. Angular 2.0 shipped on 14 September 2016. AngularJS entered long-term support in July 2018, had it extended by six months during the pandemic, and lost it on 31 December 2021; its final release, 1.8.3, is called “ultimate-farewell”. Every automated migration Angular has shipped since is, in part, an answer to 2014.

The third and fourth cases are the same story told twice. NgModules arrived in August 2016, in the fifth , five weeks before 2.0 — mostly because the compiler needed a context to know which components a template was allowed to use. For eight years they were the concept newcomers found hardest, and the part of every tutorial most likely to be wrong. What removed them was a new rendering engine. Ivy, the default from v9 in February 2020, compiles each component with everything it needs to know about itself, so a component can list its own imports. Standalone components became stable in v15 and the default in v19, in November 2024. The engine swap had a cost of its own: libraries compiled for the old engine, View Engine, ran through a compatibility compiler until v16 removed it in May 2023, with the changelog’s plain warning — “Angular View Engine libraries will no longer work.” An application can move no faster than its slowest dependency, and some teams found theirs had stopped being maintained.

Where it degrades

Angular’s reputation for being heavy is mostly about the first two weeks. The concept load really is front-loaded — dependency injection, decorators, signals, RxJS, change detection — and a developer who has only used React will be slower for a fortnight. What that buys is a fifth year that looks like the first. When an Angular code base does degrade, it is rarely the framework’s machinery that fails; it is the older column of the table living on, or a feature used against its grain.

  • Subscriptions that are never ended, which keep destroyed components and their DOM alive.
  • Function calls in templates, which run on every check rather than when the data changes.
  • OnPush with inputs changed in place, so the component never updates and looks broken at random.
  • Effects that set signals, which is a computed with a moment of disagreement built in.
  • Nested subscriptions instead of switchMap, which bring back the race the operator exists to remove.
  • Per-screen state in a root service, which survives navigation and leaks between visits.
  • bypassSecurityTrust… on content that came from users.
  • New code written in NgModules and *ngIf, which is the subject of the next migration.
  • A version outside the support window, where the next security advisory has no patch.

The honest limit is the framework’s own premise. Angular decides for you, and that is only an advantage while its decisions fit your problem. A mostly static content site pays for a full framework runtime it does not need; a small team that wants to try a new state library or router every year will fight the framework rather than use it. And uniformity cuts both ways: every Angular application inherits the same migration schedule, whether or not the team has time for it that quarter.

The practical check is two commands. ng version says which row of the release table you are on, and whether you are inside the window. ng update lists what is waiting and runs the migrations — and doing it at every major, rather than every third, is the difference between an afternoon and a project.

When to use it

Use it when

  • A large organisation with several teams that benefits from one answer per question.
  • A long-lived product, where migrations across major versions are worth real money.
  • Complex forms and coordinated asynchronous flows — the framework’s strongest ground.
  • The team wants TypeScript and dependency injection as a given rather than a choice.

Avoid it when

  • A small project or a prototype, where the concept load is paid up front and never recovered.
  • A team that wants to assemble its own stack — it will fight the framework rather than use it.
  • A mostly static content site, where a full framework runtime buys nothing.
  • No one will own upgrades — a version left behind loses its security fixes within two years.

Sources and method

What this page stands on

  • 9 sources
  • 1 documented failure
  • Checked October 13, 2026

Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.

Found this useful?

Share it with someone who is working on the same problem.