Back to Blog
AngularJune 6, 202612 min read

Angular 19+. What Really Changed and What I Use in Production

DR

Doug Ribeiro

12 min read

Angular moves fast. Between major versions there are meaningful changes to the reactive model, the rendering architecture, and the developer experience. Keeping up with the Angular changelog and deciding what goes into a regulated, enterprise production codebase are two very different activities. What follows is the second one: what I run, what I left on the shelf, and what the tradeoff was in each case.

The context matters: my Angular work has been mostly enterprise, in banking, insurance, fintech and healthcare, and I also build MailHive, my own SaaS, solo on Angular. Both taught me the same filter: stability is a primary concern. I don't adopt experimental APIs in production. I need robust testing stories. I need things that will still be the recommended approach in two years. That filter changes how I evaluate new Angular features significantly.

Core Production FeaturesSignals, standalone components, inject(), functional guards 1 Performance ToolingLazy loading, deferrable views (@defer), SSR hydration 2 State ManagementSignal-based stores, lightweight alternatives to NgRx 3 Experimental / Previewresource() and linkedSignal() (experimental in 19), effect() (developer… 4

Signals: Stabilized and In Production

Signals went stable in Angular 17. By Angular 19 the parts I depend on were stable with them: signal inputs, signal outputs, and toSignal. linkedSignal() and the resource API were still experimental in 19, and effect() was still developer preview, which is why they are not in the list below. Signals are the change I adopted earliest and most fully. MailHive's front end is built on signal-based components and signal-based state, and through beta the experience has been largely positive.

What I Use

  • signal() for local component state that drives template rendering
  • computed() for derived values. This is cleaner than any pipe or getter combination I was using before
  • input() signal API instead of @Input() decorator. Better TypeScript integration, required inputs are type-safe
  • output() instead of @Output(). Less boilerplate, consistent with the input() pattern
  • toSignal() for converting observables from services into signals for template consumption
  • takeUntilDestroyed(). Not technically signals-specific but pairs naturally with this reactive model

What I'm More Cautious About

The resource API (resource() and rxResource()) for data fetching is interesting but I'm not using it in production yet. The mental model it introduces is clean, but it's newer and I've seen the API surface shift between minor versions. In an enterprise context, I'd rather use a service with an observable and toSignal() at the consumption point. It's more explicit and more testable with the tools most teams already have.

linkedSignal() was experimental in 19 and went stable in v20, so the version argument against it is gone. My reservation now is readability, not stability. The use case is clear: a signal whose default value is derived from another signal and can be overridden. I have seen it described as confusing in code review, and I reach for it only when the alternative is a manual effect() that writes back into a signal. Simplicity in production code is a feature.

Defer Blocks: Genuinely Useful

The @defer block, introduced in Angular 17, is one of the features I was most skeptical about and have since become a genuine fan of. The ability to lazily load a piece of the template, and specify the triggers and placeholder states declaratively, solves a real UX problem cleanly.

html
@defer (on viewport) {
  <app-analytics-dashboard />
} @placeholder {
  <div class="skeleton skeleton--chart"></div>
} @loading (minimum 200ms) {
  <app-spinner />
} @error {
  <app-error-message message="Failed to load dashboard" />
}

In practice, it earns its place for: below-the-fold dashboard widgets, heavy chart components, content that requires user interaction to be relevant (tabs, accordion sections), and third-party embedded components with large bundle footprints. The on viewport trigger in particular is extremely useful for dashboard layouts where multiple widgets compete for initial load time.

The testing story for @defer is good enough for production: you can control the defer state in tests and test each state (placeholder, loading, content, error) independently. That is a prerequisite for adopting it in any codebase where test coverage expectations are high.

Incremental Hydration: Stable Since v20, Still Not In My Stack

Angular 19 introduced incremental hydration as a developer preview and v20 promoted it to stable. It hydrates server-rendered content in pieces, by part of the template, instead of all at once. Combined with @defer, this means you can defer both the initial render and the hydration of specific parts of the page.

This is technically impressive. The performance implications for content-heavy pages are significant. But here is my honest production assessment: most of the Angular I have shipped does not use SSR at the level of sophistication that makes incremental hydration a priority concern. It is SPA (single-page application) work, application flows and dashboards, not SEO-critical content pages. The feature is stable and ready. My workloads are not the ones it pays off on.

If you're building a public-facing Angular application with SSR for SEO or performance, an e-commerce storefront, a marketing site, a content platform, incremental hydration is worth serious investment right now. If you're building an authenticated enterprise dashboard, it's a feature to understand and file away for when the use case arises.

Route-Level Render Modes

Angular 19 introduced route-level render mode configuration in developer preview, and v20 made it stable. In a server route config (app.routes.server.ts), you specify per route whether it should be: server-rendered (SSR), pre-rendered (SSG), or client-rendered (CSR). This is a meaningful improvement over the previous all-or-nothing SSR configuration.

typescript
// app.routes.server.ts: render mode per route, next to the normal app.routes.ts
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Prerender },
  { path: 'dashboard', renderMode: RenderMode.Client },  // authenticated, no SSR needed
  { path: 'about', renderMode: RenderMode.Prerender },
  { path: '**', renderMode: RenderMode.Server }
];

The mental model it enables is clean: public pages get SSR/SSG for performance and SEO, authenticated application pages get CSR. For applications that span both categories, a product with a public landing and a private dashboard, this is the right architecture and Angular now supports it cleanly.

Standalone Components: Fully Adopted, No Regrets

This is less Angular 19 and more the longer arc from Angular 14-17, but standalone components are now my default for everything. NgModules are gone from new code I write. In codebases that still carry them, I migrate existing modules gradually rather than in one big pass.

The benefits I've experienced in production: smaller bundles (tree-shaking is more effective), clearer dependency graphs (you see exactly what each component needs), simpler testing (no TestBed module setup for components that don't need it), and easier code splitting aligned with route-level lazy loading.

The one friction point: large codebases with deeply nested NgModules take real effort to migrate incrementally. Angular provides schematics and migration tooling, and they work well for clean cases. The complex module hierarchies with cross-module shared providers need careful manual attention. Budget time for this if you're in the middle of a migration.

OnPush Change Detection and the Signal Interaction

One thing I want to address directly because it is a common source of confusion: OnPush change detection and signals interact in a specific way that you need to understand.

With OnPush, a component re-renders when: its input reference changes, an event originates from it, an async pipe resolves, or markForCheck() is called. With signals, there's an additional trigger: when a signal read during the previous render changes value, Angular marks the component for check automatically. Even with OnPush.

This is the right behavior, but it means OnPush plus signals is the correct combination, not a redundant one. You keep OnPush for its other benefits (reducing default change detection cycles) and signals add fine-grained reactive updates on top. I use OnPush on every component I write, and on MailHive it has been well-behaved with signals through beta. As of v22 that stance is the framework's too: OnPush is now the default change detection strategy for new components, and the old always-check default was renamed Eager. If you are on v22 you inherit this. If you are on v21 or earlier, keep setting it by hand, and know that the migration to v22 makes your un-annotated legacy components stricter, not looser.

The Control Flow Syntax

Angular 17 introduced the new template control flow syntax (@if, @for, @switch) to replace *ngIf, *ngFor, and *ngSwitch. By Angular 19 this is the clearly preferred approach and I've migrated all new code to it.

html
@for (account of accounts(); track account.id) {
  <app-account-card [account]="account" />
} @empty {
  <p class="empty-state">No accounts found.</p>
}

The track requirement in @for is a breaking change in the right direction. Forcing you to specify tracking prevents the performance issues we used to see with untracked lists. The @empty block is a quality-of-life improvement that eliminates a common pattern of a separate *ngIf for the empty state.

What I'm Waiting to See Mature

  • The resource() API: v22, released days before this post, marks it stable, and the concept is right, but I want to see it hold steady through one more major version before using it in production over observable-based patterns
  • Angular Material 3 in established design systems: M3 has been stable in Angular Material since v18, but moving an established theme to M3 tokens is a design system decision. When another team owns the design system, that call is theirs, not the front-end developer's; I watch it so I know what to expect when it lands
  • Signal Forms: experimental in Angular 21 and stable as of v22. They will be a major DX improvement; until they have some real-world mileage, the FormControl + toSignal() bridge works but it's a bridge, not a native integration

Final Take

Angular 19+ is a genuinely good framework. The combination of signals, standalone components, defer blocks, and the new template syntax produces code that is more readable, more performant, and more testable than what I was writing two major versions ago. The opinionated structure that sometimes gets criticized is exactly what I want in an enterprise context.

My production adoption philosophy: signals and standalone fully in. Defer blocks for appropriate lazy-loading use cases. New control flow syntax for all new templates. Incremental hydration and route render modes when SSR becomes a priority. Resource API when it's been stable for a version or two.

You don't have to use every new feature to get value from a major release. Being deliberate about what you adopt and why is the mark of a senior developer.

References

AngularTypeScriptFront-EndProduction