On this page
Key takeaways
- Build new apps with standalone components; migrate NgModule apps step by step with the CLI's migrations.
- Use signals for component state (
signal,computed,input(),output()), and RxJS for events and async streams. - Use the built-in control flow (
@if,@forwithtrack) and@deferto load heavy parts of a page only when needed. - Make components
OnPush; with signals, new apps can run without zone.js. - Keep the 2022 basics: small single-purpose files, Prettier, lazy-loaded routes, presentational components, and no leaked subscriptions.
In 2022 we wrote down the Angular best practices our team had learned from four years of enterprise apps. Since then Angular has changed more than in any period since Angular 2: standalone components, signals, a new template syntax and a faster build. This post covers what we do on new Angular apps in 2026, and which of our 2022 practices still hold.
Our Angular work includes the clinician app for Graham Healthcare Group, a progressive web app for their home-care clinicians, and products for MadDog Technology and Solxsys.
What still holds from 2022
These haven't changed, and we still apply them on every project:
- One thing per file. One component, service or directive per file, named after what it is.
- Let tools do the formatting. Prettier with a pre-commit hook, plus ESLint through angular-eslint (
ng add @angular-eslint/schematics). - Lazy-load features. The first download should contain only what the first screen needs.
- Smart and presentational components. Screens load data and handle logic; the components inside them just take inputs and raise outputs.
- Don't leak subscriptions. Use the async pipe,
take(1), ortakeUntilDestroyed(). - No expensive work in templates. Templates should read values, not compute them.
- Path aliases in
tsconfig.json(@core/*,@shared/*) instead of long../../../imports.
What changed is how you do most of these. The rest of this post is the 2026 version.
1. Standalone components, not NgModules
Since Angular 19, components, directives and pipes are standalone by default. A component imports what it uses directly, and the app starts with bootstrapApplication() instead of an AppModule:
// main.ts
bootstrapApplication(App, {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor])),
],
});
@Component({
selector: 'app-order-list',
imports: [CurrencyPipe, OrderRow],
templateUrl: './order-list.html',
})
export class OrderList { }
Existing apps: you don't need to rewrite anything. NgModules are still supported, and the two styles work side by side. ng generate @angular/core:standalone migrates an app in three passes (convert components, remove unneeded modules, switch to bootstrapApplication). We run it feature by feature, with a build and a test run between passes.
2. The new style guide: shorter names
Angular rewrote its style guide in 2025 (Angular 20). The biggest visible change: no type suffixes for components, directives and services. The CLI now generates order-list.ts with class OrderList, not order-list.component.ts with OrderListComponent. Other points:
- Use the
inject()function instead of constructor injection. - Keep components focused on presentation; move logic into services or plain functions.
- Use
protectedfor members that only the template uses. - Group files by feature, not by type.
If an existing project uses suffixes, keep them there. Consistency within a codebase matters more than following the newest guide.
3. Signals for state
Signals are Angular's built-in reactive values. A signal holds a value, a computed() signal derives one, and Angular updates exactly the parts of the page that read them:
export class Cart {
items = signal<CartItem[]>([]);
total = computed(() => this.items().reduce((sum, i) => sum + i.price * i.qty, 0));
add(item: CartItem) {
this.items.update(items => [...items, item]);
}
}
<p>Total: {{ total() | currency }}</p>
Calling total() in the template is fine. That's how signals are read, and Angular only re-evaluates it when items changes. (Our 2022 advice against functions in templates still applies to ordinary methods.)
Components use signal-based inputs, outputs and queries:
export class LessonsList {
lessons = input.required<Lesson[]>();
selected = output<Lesson>();
search = model(''); // two-way binding
}
Practices we follow:
- Use
computed()for derived values, noteffect()that sets another signal. - Keep
effect()for side effects that leave Angular: logging, local storage, a third-party chart. - Update signals immutably (
update(items => [...items, item])), socomputed()andtracksee the change. - Use
linkedSignal()for a value that resets when another changes (for example, the selected item when the list changes).
4. Signals and RxJS: use both
Signals don't replace RxJS. Our rule of thumb:
| Use signals for | Use RxJS for |
|---|---|
| Component and screen state | HTTP calls, websockets, server events |
| Values the template reads | User input over time (search-as-you-type with debounceTime + switchMap) |
Derived values (computed) | Combining, retrying or cancelling async work |
Convert at the boundary with RxJS interop: toSignal(this.http.get(...)) to show the result in a template, toObservable(signal) to feed a signal into an RxJS pipeline. For loading data, resource() and httpResource() put async results straight into signals, with loading and error states built in; they're stable since Angular 22.
5. Built-in control flow
Angular 17 added control flow blocks to templates, and they're now the standard. They replace *ngIf, *ngFor and *ngSwitch (deprecated since Angular 20):
@if (user(); as user) {
<p>Welcome {{ user.name }}</p>
} @else {
<a routerLink="/login">Sign in</a>
}
@for (order of orders(); track order.id) {
<app-order-row [order]="order" />
} @empty {
<p>No orders yet.</p>
}
@for requires track, which fixes the most common list performance problem from our 2022 post. Track by a unique id, not $index. ng generate @angular/core:control-flow converts existing templates.
6. @defer: lazy-load inside a page
Route-level lazy loading is still the first step (loadComponent for one component, loadChildren for a feature's routes). @defer goes further and lazy-loads part of a template:
@defer (on viewport) {
<app-sales-chart [data]="sales()" />
} @placeholder {
<div class="chart-placeholder"></div>
} @loading (minimum 300ms) {
<app-spinner />
}
The chart's code isn't downloaded until it scrolls into view. Good candidates: charts, rich text editors, maps, and anything below the fold or behind a tab.
7. Change detection: OnPush and zoneless
- Make every component
OnPush. With signals and inputs, it's almost free, and it stops Angular from checking components whose data hasn't changed. Set it as the CLI default inangular.json. - Zoneless. Angular no longer needs zone.js to know when to update the page; signals, events and the async pipe tell it directly. Zoneless is the default for new apps since Angular 21. For an existing app, remove zone.js only after its components are OnPush and signal-based, then test the screens that update after timers or third-party callbacks.
- Large lists: use
@forwithtrack, and the CDK's virtual scrolling (cdk-virtual-scroll-viewport) for thousands of rows.
8. State management: start simple
Most business apps don't need a state library on day one. What we use:
| App size | Approach |
|---|---|
| A few screens with their own data | Signals in the component |
| State shared between screens | A service holding signals (private signal, public asReadonly() and computed) |
| Large apps, many teams, complex shared state | NgRx Signal Store; classic NgRx Store where an app already uses it |
NgRx's older Component Store is still supported, but for new code we use Signal Store.
9. Forms
In existing code, use typed reactive forms (since Angular 14): FormControl<string> catches mistakes at compile time that untyped forms only show at runtime. Use nonNullable controls so reset() doesn't produce null. For new forms, Signal Forms build the form model on signals and are stable since Angular 22.
10. Build, SSR and performance
- Build: the
applicationbuilder (esbuild and Vite) is the default. Moving an older app off the webpack builder usually cuts build times a lot.ng updateoffers the migration. - Server-side rendering: for public, search-indexed pages,
@angular/ssrwith hydration (and incremental hydration with@defer) gives a faster first paint and better SEO. Internal business apps behind a login usually don't need it. - Images:
NgOptimizedImage(ngSrc) sets sizes, lazy-loads below the fold and prioritizes the main image. - Budgets: keep the bundle budgets in
angular.jsonand fail the build when they're exceeded.
11. Testing
- Unit tests: Vitest is the default test runner for new CLI projects (see Angular's testing guide); Karma is still supported for existing ones. Test components through their template (what the user sees), not their private methods.
- Signals in tests: set inputs with
fixture.componentRef.setInput(), then check the rendered output. - End-to-end: Playwright or Cypress for the main user flows. Protractor is gone.
12. Security basics
Angular escapes values in templates by default, so most cross-site scripting needs a bypass to happen. Don't use bypassSecurityTrust* with user data, keep HTTP calls on HttpClient so its XSRF support works, and set a Content Security Policy.
13. Keep up with releases
Since Angular 22, a new major version comes out every 12 months (it was every 6 months before), and each one gets about 24 months of support: 12 months of active support, then 12 months of long-term support (see versioning and releases). Upgrade at least once a year; skipping several versions is how a routine ng update becomes a project. ng update runs migrations for you, one major version at a time, and Angular's Update guide lists the manual steps for each version.
Frequently asked questions
What are the most important Angular best practices in 2026?
Standalone components, signals for state, the built-in control flow with track, @defer and route lazy loading, OnPush change detection, and typed or signal-based forms. The basics still apply: small files, Prettier and ESLint, and presentational components.
Should I still use NgModules?
Not for new code. Existing NgModule apps are supported and can move to standalone gradually with ng generate @angular/core:standalone.
Do signals replace RxJS?
No. Use signals for state the template reads and RxJS for async streams such as HTTP calls and user input. toSignal() and toObservable() connect them.
Do I need NgRx?
Usually not at the start. Signals in components and services cover most business apps. Consider NgRx Signal Store when shared state grows across many screens or teams.
How often should we upgrade Angular?
At least once a year. A new major version comes out every 12 months and is supported for about 24 months, and ng update upgrades one major version at a time.
Is it worth migrating an existing Angular app to signals and standalone?
Yes, gradually. The CLI migrations do most of the mechanical work; plan the time for testing. Start with the screens you're already changing.
Need help with an Angular app?
We build Angular front ends for business applications and upgrade older Angular apps (including AngularJS) to current versions. See our software development and application modernization services, or talk to an engineer at our Troy, Michigan office.
Join the discussion
No comments yet.
Leave a Reply