What Angular is
Angular is a TypeScript framework for building web applications, maintained by Google and released on a six-month cadence. It ships with routing, forms, an HTTP client, a testing setup and a build system in the box, which is the main thing that separates it from the lighter alternatives that leave those decisions to you.
The version this sheet targets is Angular 22, released in June 2026. If you learned Angular before version 17, a lot of what you know still works, but the recommended way to write almost everything has changed.
Three shifts define modern Angular:
- Standalone components. Components declare their own template dependencies through an
imports array. NgModules still work, but nothing new needs them.
- Signals. State is held in signal objects that track their own readers, so Angular knows precisely which views to update.
- Zoneless change detection. With signals doing the tracking, zone.js is no longer required. New Angular 22 projects leave it out by default.
Signals, in one pass
A signal is a value with a getter. You read it by calling it, and any template or computation that reads it is registered as a dependency.
count = signal(0);
// read
this.count();
// write
this.count.set(5);
this.count.update(c => c + 1);
There are three pieces to learn, and most Angular code uses only these:
signal() holds state you write to.
computed() derives state from other signals. It is lazy and memoized, so it recalculates only when a dependency actually changed and something reads the result.
effect() runs a side effect when its dependencies change. Use it for logging, storage and imperative DOM work, not for deriving values.
The rule that saves the most debugging time: if you are tempted to set one signal inside an effect, you almost certainly want computed() or linkedSignal() instead.
Why signals replaced the old model
Before signals, Angular relied on zone.js to patch every asynchronous browser API and then re-check the component tree after anything happened. It worked, but it checked far more than necessary and it made performance hard to reason about. Signals invert that: the framework is told exactly what changed and which views read it.
Component syntax you will write daily
A modern component is a class with a decorator, no module in sight.
@Component({
selector: 'app-user-card',
imports: [DatePipe],
template: `
<h2>{{ user().name }}</h2>
<p>Joined {{ user().joined | date }}</p>
`,
})
export class UserCard {
user = input.required<User>();
removed = output<string>();
}
Note what is absent: no standalone: true (it is the default since v19), no changeDetection line (OnPush is the default in v22), and no constructor.
Inputs, outputs and two-way binding
Signal-based input() and output() replaced the @Input() and @Output() decorators. Inputs read like any other signal, which means they compose directly into computed() without an ngOnChanges hook.
For two-way binding, model() creates a writable signal that also emits a matching change event, so a parent can write [(checked)]="isOn" against a child that declares checked = model(false).
Template control flow
Angular 17 introduced block syntax that replaced the structural directives. It is built into the compiler, so there is nothing to import.
@if (user(); as u) {
<p>{{ u.name }}</p>
} @else {
<a routerLink="/login">Sign in</a>
}
@for (item of items(); track item.id) {
<li>{{ item.label }}</li>
} @empty {
<li>Nothing here yet.</li>
}
The track expression is required, and it matters more than it looks. Angular uses it to decide which DOM nodes to keep between renders. A stable unique identifier keeps the list cheap; tracking by index on a reordering list forces Angular to rebuild rows that did not change.
Deferred loading
@defer is the piece with no equivalent in the old directive set. It splits a chunk of template into its own lazy bundle and gives you declarative triggers for when to fetch it.
@defer (on viewport; prefetch on idle) {
<app-comments />
} @placeholder (minimum 500ms) {
<div class="skeleton"></div>
} @loading (after 100ms) {
<app-spinner />
} @error {
<p>Could not load comments.</p>
}
Triggers include on idle, on viewport, on interaction, on hover, on timer, on immediate and when with any expression. Add hydrate on ... and the same block controls incremental hydration on a server-rendered page.
Data loading with resources
The resource API became stable in Angular 22 and is now the shortest path from an async source to loading, error and value state.
userId = signal(1);
user = httpResource<User>(() => `/api/users/${this.userId()}`);
// template
@if (user.isLoading()) {
<app-spinner />
} @else if (user.hasValue()) {
<h1>{{ user.value().name }}</h1>
}
Three flavours cover most needs. resource() takes a promise loader, rxResource() takes an observable stream, and httpResource() wraps HttpClient. All three re-run when a signal read inside their parameters changes, and all three hand you an abortSignal so the previous request is cancelled rather than raced.
HttpClient has not gone anywhere. Use it directly when your API integration needs interceptors, upload progress, or full control over the response. Use a resource when what you want is state bound to a template.
Forms: two stacks, one recommendation
Angular now ships two form systems, and the choice is mostly about when your project started.
| Concern | Signal Forms | Reactive Forms |
| package | @angular/forms/signals | @angular/forms |
| source of truth | a signal holding your model | a FormGroup tree you build |
| validation | schema function next to the model | validators attached per control |
| arrays | plain arrays plus applyEach | FormArray API |
| state | signals | observables and getters |
| status | stable in v22 | stable, fully supported |
Signal Forms invert the usual setup. Instead of describing a form and then syncing it with your data, you start from the data and let the form structure follow.
model = signal({ email: '', password: '' });
loginForm = form(this.model, (path) => {
required(path.email, { message: 'Email is required' });
email(path.email, { message: 'Enter a valid email' });
minLength(path.password, 8);
});
Reactive Forms are not deprecated and large applications should not rush a rewrite. Start new forms on the signal API, keep existing ones where they are.
Routing
Routes are plain objects, registered with provideRouter() at bootstrap. Lazy loading is a function that returns a dynamic import, and guards are ordinary functions that can call inject().
export const routes: Routes = [
{ path: '', component: Home, title: 'Home' },
{
path: 'admin',
loadComponent: () => import('./admin').then(m => m.Admin),
canMatch: [adminGuard],
},
{ path: '**', component: NotFound },
];
Two features are worth turning on early. withComponentInputBinding() maps route params, query params and resolved data straight onto component inputs, which removes most ActivatedRoute boilerplate. And canMatch beats canActivate for protected lazy routes, because it runs before the chunk downloads.
Dependency injection
Angular's dependency injection resolves services through a hierarchical injector tree. A service marked providedIn: 'root' is a tree-shakable singleton: if nothing injects it, it never reaches the bundle.
The inject() function has largely replaced constructor parameters. It works in field initializers, reads better with inheritance, and lets guards, resolvers and interceptors be plain functions instead of classes.
export class UserList {
private users = inject(UserService);
private route = inject(ActivatedRoute);
}
Providing a service on a component instead of at the root gives every instance of that component its own copy, which is the standard way to scope wizard state, panel state or a per-dialog store.
Performance: what actually moves the numbers
- Track by a stable key. The single most common cause of sluggish lists is a track expression that changes when the data reorders.
- Defer below the fold.
@defer (on viewport) keeps charts, comment threads and editors out of the initial bundle.
- Go zoneless. Dropping zone.js removes a polyfill and stops the framework from checking views that nothing touched.
- Prioritize the LCP image.
NgOptimizedImage with priority preloads the largest image and warns when dimensions are missing.
- Keep function calls out of templates. Anything written as
{{ compute() }} may run on every check. Wrap it in a computed() instead.
- Set budgets. A build that fails on bundle size catches regressions that code review will not.
Version timeline
| Version | Released | What it introduced |
| v16 | May 2023 | Signals in developer preview, required inputs, standalone APIs maturing |
| v17 | Nov 2023 | Built-in control flow, @defer, new build system, angular.dev |
| v18 | May 2024 | Zoneless in experimental, @let, ng-content fallbacks |
| v19 | Nov 2024 | Standalone by default, linkedSignal, resource preview, incremental hydration |
| v20 | May 2025 | Signal APIs stable, native animate.enter and animate.leave, httpResource |
| v21 | Nov 2025 | Zoneless stable, Vitest, Signal Forms experimental, Angular Aria preview |
| v22 | Jun 2026 | Signal Forms and resources stable, OnPush by default, selectorless components, Angular Aria stable |
Angular's release cycle gives each major 18 months of support: six months active, then twelve months of long-term support with security and critical fixes only.
Migrating an older codebase
Angular ships codemods that do most of the code refactoring for you, so this step is largely mechanical. Run them one at a time and make a Git commit between each, so your unit tests can tell you exactly what moved.
ng update @angular/core @angular/cli
ng generate @angular/core:standalone
ng generate @angular/core:control-flow
ng generate @angular/core:inject
ng generate @angular/core:signal-inputs
ng generate @angular/core:signal-queries
ng generate @angular/core:output-migration
ng generate @angular/core:cleanup-unused-imports
Upgrade one major at a time rather than jumping several versions. The update tool only knows how to migrate between adjacent releases, and skipping versions means skipping the schematics that would have done the work for you.
What not to rush
Reactive Forms, NgModules in stable feature areas and working RxJS pipelines are all still supported. Angular has been careful about removals, and rewriting working code for no user-facing benefit does not pay down technical debt, it manufactures risk. Convert when you are already touching the code.
Common mistakes
- Forgetting to call the signal.
{{ count }} renders the function itself. It needs {{ count() }}.
- Setting signals inside effects. This creates feedback loops that are hard to trace. Derive with
computed().
- Subscribing without cleanup. Use
takeUntilDestroyed() or the async pipe rather than storing subscriptions by hand.
- Missing imports on standalone components. If a directive or pipe silently does nothing, it is almost always absent from
imports.
- Wildcard route placed too early. Routes match top to bottom, so
** must be last.
- mergeMap where switchMap belongs. For search-as-you-type,
mergeMap lets stale responses arrive after fresh ones.
- Touching the DOM in
ngOnInit under SSR. Use afterNextRender(), which never runs on the server.
Reading the version badges
Every snippet carries the earliest Angular version it works in. A badge reading v19+ means the API landed in Angular 19 and still works today. A badge reading core means it predates the signals era and works in every supported release.
The filter in the header answers the question you actually have. Pick the version you are running and the sheet hides anything you cannot use yet. Snippets marked legacy still work but are no longer the recommended approach, and the toggle next to the version filter removes them.
A handful of APIs are marked experimental. Angular ships those outside its usual semantic versioning promise, so they can change in a patch release. In Angular 22 that applies to debounced(), injectAsync and selectorless components.
Accessibility with Angular Aria
Angular Aria became stable in v22. It is a set of headless directives that implement the WAI-ARIA patterns most teams get subtly wrong: keyboard interaction, focus management, and the correct roles and states.
Headless means it ships no styles at all. You own the markup, the CSS and the UI/UX design, and the directives handle behaviour. Because they set the ARIA attributes for you, those attributes double as your styling hooks:
[aria-selected='true'] { background: #e8f0fe; }
[aria-expanded='true'] .icon { transform: rotate(180deg); }
It is a separate install, npm install @angular/aria, with entry points per pattern: accordion, combobox, grid, listbox, menu, tabs, toolbar and tree.
Internationalization
Angular's built-in i18n is compile-time. You mark text with the i18n attribute, extract it to a translation file, and your build pipeline emits one bundle per locale. There is no runtime language switch, which is why the bundles are small and there is no flash of untranslated content.
<h1 i18n="site header|Greeting after login@@homeWelcome">Welcome back</h1>
The value is meaning|description@@id, and all three parts are optional. Setting an explicit id is worth the keystrokes: without one, Angular generates an id from the source text, so fixing a typo in English silently orphans every translation of that string.
The workflow is ng extract-i18n, then ng build --localize once the translated files are listed in angular.json. If you need to switch language at runtime without a page load, you want a library such as Transloco or ngx-translate instead.
Security defaults
Angular escapes interpolated values, so {{ userInput }} can never inject markup. Binding to [innerHTML] runs the value through the sanitizer, which strips script tags and inline event handlers.
The escape hatch is DomSanitizer. Treat every call to bypassSecurityTrustHtml or its siblings as something that needs a second pair of eyes: the method name is a promise you are making to the framework, and it is only safe on values you constructed yourself.
Two things Angular does not do for you: it does not validate anything on your back end, and it does not set your Content Security Policy. For CSP, ngCspNonce lets Angular tag the styles it injects with your per-request nonce.
Keyboard and usage notes
Press / anywhere on the page to focus the search box and Escape to clear it. Search is ranked, so a match in a snippet's description outranks a match buried in its code, and it tolerates a single typo when nothing matches exactly. With results on screen, Arrow Up and Arrow Down step through them and Enter copies the highlighted one.
The topic rail is a tab list: once one topic has focus, the arrow keys move between them and Home and End jump to the ends.
Every view has its own link. Opening a topic or running a search updates the address bar, so you can bookmark #ngcs=signals or send someone #ngcs=search&q=defer&v=19 and they will land on exactly what you were looking at, filter included. Browser back and forward work as expected.
Every snippet has a copy button. Snippets with a Why button carry a note explaining a gotcha, a default that changed, or the reason one approach beats another. The page prints cleanly too: the chrome and controls drop away, all topics expand, and the notes stay visible.