← Back to blog
Angular
Signals
Frontend Architecture
Signal Store
Zoneless
SSR
TypeScript
Accessibility

Modern Angular Development in 2026: A Senior Developer's Guide

By Sujit Yadav16 min read
Modern Angular Development in 2026: A Senior Developer's Guide

Angular in 2026 is not the framework most teams think they are using. The name is the same, the CLI is the same, and the upgrade path has been remarkably kind. But underneath, the reactivity model, the change detection strategy, the component contract and the rendering pipeline have all been replaced. You can run a modern Angular version and still write 2019 Angular — and a surprising number of production codebases do exactly that.

That is the real divide I see when I audit Angular applications. It is almost never between teams on old versions and teams on new ones. It is between teams who upgraded the version number and teams who changed the model. The first group gets a slightly faster build. The second group gets smaller bundles, fewer bugs, simpler code and applications that stay maintainable past the three-year mark.

This guide is the map I use with the second group. It covers what modern Angular actually is, in the order I would adopt it, with the trade-offs stated honestly. It is deliberately opinionated: after eight years of building enterprise Angular for aviation safety, healthcare and compliance products, I have watched most of the alternatives fail in slow motion.

The one shift that explains all the others

Classic Angular had a simple deal: you mutate whatever you like, and Zone.js patches every async primitive in the browser to tell the framework that something, somewhere changed. Angular then walked the component tree to find out what. It worked, and it was wasteful, and it was impossible to reason about locally.

Modern Angular replaces that with signals: values that know who depends on them. When a signal changes, only the computations and the views that actually read it are marked dirty. Nothing patches the browser. Nothing walks the whole tree.

Every other change in this guide follows from that one. Zoneless change detection is possible because of signals. The new inputs and outputs exist to make component boundaries signal-aware. SignalStore exists to give that model a place to hold business state. If you take one thing from this article, take this: adopt signals first, and the rest becomes obvious.

1. Signals: the reactive core

There are three primitives and you should treat them as having very different privileges.

import { signal, computed, effect } from '@angular/core';

// 1. Writable state. The only thing you mutate.
const quantity = signal(1);
const unitPrice = signal(49.9);

// 2. Derived state. Pure, lazy, cached, glitch-free.
const total = computed(() => quantity() * unitPrice());

// 3. Side effects. Synchronising with the world OUTSIDE Angular.
effect(() => localStorage.setItem('quantity', String(quantity())));

The discipline that separates senior codebases from junior ones is the ratio between these three. Aim for a lot of computed, a little signal, and almost no effect.

The rule about effects

An effect is not a reactive tool. It is an escape hatch to the non-reactive world: logging, analytics, the document title, a canvas, a third-party charting library. The moment you write an effect that sets another signal, you have reintroduced exactly the untraceable cascade that signals were designed to eliminate.

// Anti-pattern. Two sources of truth, one frame apart.
effect(() => {
  this.total.set(this.quantity() * this.unitPrice());
});

// Correct. One source of truth, no timing.
readonly total = computed(() => this.quantity() * this.unitPrice());

If you find yourself reaching for an effect to derive state, the answer is almost always computed. If it needs to be derived and writable, the answer is linkedSignal.

linkedSignal: derived state that a user can override

This is the primitive that closes the most common gap. A shipping option that defaults from the selected country but can be changed by hand. A quantity that resets when the product changes. Before, that meant an effect and a bug report.

readonly shippingOptions = input.required<ShippingOption[]>();

// Resets to the first option whenever the source changes,
// but the user can still .set() their own choice.
readonly selected = linkedSignal(() => this.shippingOptions()[0]);

I wrote a dedicated walkthrough of the API and its scheduling behaviour in Exploring Linked Signals in Angular 19, including the source / computation form for cases where you need the previous value.

Async state

Angular's resource and rxResource APIs model asynchronous data as a signal with value, status and error, which removes an enormous amount of hand-rolled loading-flag code. Check the stability status for your exact version before you standardise on them — they arrived experimental in v19 and have been settling since. Where they are not yet appropriate, keep the request in an RxJS stream and convert at the boundary; interoperability is deliberate and cheap.

// RxJS stays excellent for event streams, cancellation and retries.
// Signals are better for state. Convert at the edge, don't pick a side.
readonly user = toSignal(this.userStream$, { initialValue: null });

2. The component contract

Standalone components are the default now, and the decorator-based inputs and outputs have signal equivalents that are strictly better: they are typed more precisely, they are readable in computed without ceremony, and required inputs are enforced at compile time.

@Component({
  selector: 'app-invoice-row',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (isOverdue()) {
      <span class="text-red-600">Overdue</span>
    }
  `,
})
export class InvoiceRowComponent {
  // Required — the compiler rejects callers that omit it.
  readonly invoice = input.required<Invoice>();
  readonly currency = input('EUR');

  // Two-way binding without a matching Change output.
  readonly expanded = model(false);

  readonly remove = output<string>();

  readonly isOverdue = computed(() => this.invoice().dueDate < new Date());
}

Three practical notes from code review. Use input.required aggressively — an optional input that every caller passes is a lie in your public API. Prefer model() over the value / valueChange pair, because it makes the two-way contract explicit. And use inject() rather than constructor parameters: it works in field initialisers, in functions, and in inheritance chains where constructor injection becomes a maintenance tax.

3. Change detection: OnPush today, zoneless tomorrow

Zoneless Angular removes Zone.js entirely. The bundle drops by roughly 12 KB compressed, stack traces become readable, and change detection runs only when a signal actually changes. It also stops being forgiving — code that relied on Zone.js noticing a setTimeout will simply not update.

The honest migration path is not a flag flip. It is this:

  1. Put ChangeDetectionStrategy.OnPush on every component. Not most. Every one. This is the actual work, and it is worth doing even if you never go zoneless.
  2. Remove manual ChangeDetectorRef.detectChanges() calls. Each one is a marker for state that is not reactive. Fix the state, delete the call.
  3. Move component state to signals so that OnPush has something to react to.
  4. Enable zoneless in a feature or a test harness first, then globally.
bootstrapApplication(AppComponent, {
  providers: [
    // v19: provideExperimentalZonelessChangeDetection()
    // Stabilised as provideZonelessChangeDetection() in v20+ —
    // check the API name for the version you are on.
    provideZonelessChangeDetection(),
  ],
});

Steps 1 to 3 deliver most of the performance win on their own. Step 4 is the reward for having done them properly, and it is the point at which a codebase stops being able to drift back.

4. Templates: built-in control flow and deferred loading

The block syntax is not sugar over *ngIf. It is compiled differently, it ships no directive imports, and it produces faster list diffs. Migration is one command (ng generate @angular/core:control-flow), and the track expression on @for is mandatory for a reason: it is the difference between recreating a thousand DOM nodes and moving them.

@for (invoice of invoices(); track invoice.id) {
  <app-invoice-row [invoice]="invoice" />
} @empty {
  <p>No invoices for this period.</p>
}

Then there is @defer, which is the single highest-leverage performance feature Angular has shipped. It lazy-loads a section of template and its entire dependency graph on a declarative trigger, with first-class placeholder and loading states.

@defer (on viewport) {
  <app-revenue-chart [data]="series()" />
} @placeholder (minimum 300ms) {
  <div class="h-64 animate-pulse rounded-xl bg-gray-100 dark:bg-neutral-800"></div>
} @error {
  <p>The chart could not be loaded.</p>
}

Apply it to charting libraries, rich text editors, maps, date pickers and anything below the fold. In practice this is where heavy dependencies stop damaging your Largest Contentful Paint. Note the minimum on the placeholder — without it, fast connections produce a flash of skeleton that reads as jank rather than speed.

5. Where business logic belongs: SignalStore

The most expensive architectural mistake in Angular applications is not choosing the wrong state library. It is putting business rules in components, where they cannot be tested without a TestBed, cannot be reused, and quietly duplicate themselves across three screens.

NgRx SignalStore gives you a place to put them with very little ceremony: state, derived state and methods in one unit, with immutable updates through patchState.

export const InvoiceStore = signalStore(
  withState<InvoiceState>({ invoices: [], filter: 'all', loading: false }),

  withComputed(({ invoices, filter }) => ({
    visible: computed(() => applyFilter(invoices(), filter())),
    outstandingTotal: computed(() => sumOutstanding(invoices())),
  })),

  withMethods((store, api = inject(InvoiceApi)) => ({
    async load(period: Period) {
      patchState(store, { loading: true });
      const invoices = await api.list(period);
      patchState(store, { invoices, loading: false });
    },
    setFilter(filter: InvoiceFilter) {
      patchState(store, { filter });
    },
  })),
);

Two rules keep this clean over time. First, the store orchestrates, it does not calculateapplyFilter and sumOutstanding are pure functions in their own files, unit-testable with no Angular imports at all. Second, scope deliberately: provide the store at component level for per-instance state, and at root only when it is genuinely application-wide. A root-provided store that should have been component-scoped is a memory leak and a cross-screen bug waiting to happen.

I have written about why this pattern replaces both service-with-BehaviorSubject and classic NgRx in Why Your Angular Project Needs Signal Store, and covered its introduction alongside the other platform changes in Angular Version 18: Exploring New Features and Signal Store.

6. Architecture that survives the second year

Framework features do not save a large codebase. Boundaries do. The structure I use on every non-trivial Angular application is a set of vertical domain slices, each cut into layers, inside an Nx monorepo.

libs/<app>/<domain>/
├── domain/          state + business logic (SignalStore, models, pure utils)
├── feature-<name>/  smart components — inject the store, render UI
├── ui-shared/       presentational components for this domain
└── shell/           lazy-loaded routing entry

The dependency rule runs one way only: shell → feature → domain, and anything may depend on the shared design system. Domain code never imports a component. Feature code never contains a business rule. The shell contains nothing but routes.

This is worth the up-front cost for one reason: it makes the expensive changes cheap. Replacing a screen touches one feature-* library. Changing a business rule touches one domain library and its unit tests. Onboarding a developer means pointing at one folder rather than explaining a codebase.

Critically, enforce it mechanically. Tag every library and configure @nx/enforce-module-boundaries with real depConstraints, so an illegal import fails lint rather than depending on whoever reviews the pull request that afternoon. Tags without constraints are documentation, and documentation loses to deadlines.

// eslint config — the boundary that actually holds
{
  "sourceTag": "type:feature",
  "onlyDependOnLibsWithTags": ["type:domain-logic", "type:ui"]
}

Where a shared component genuinely has to outlive the framework — a design system consumed by an Angular app and a React app, for instance — Angular Elements is a reasonable escape hatch, and I covered the trade-offs in Elevating Angular Development with Web Components.

7. Typed forms, used properly

Reactive forms have been strictly typed for several versions, and most codebases still throw that away with a stray any or an untyped FormBuilder call. Two habits recover the full benefit.

private readonly fb = inject(FormBuilder);

// nonNullable.group() — reset() returns to the initial value,
// not to null, and the type is string rather than string | null.
readonly form = this.fb.nonNullable.group({
  email: ['', [Validators.required, Validators.email]],
  vatNumber: ['', [Validators.pattern(/^[A-Z]{2}[0-9A-Z]{2,12}$/)]],
});

// Typed access — form.controls.email is FormControl<string>
readonly emailInvalid = computed(() => ...);

Use nonNullable by default. Avoid form.get('email') in favour of form.controls.email, because the string lookup returns AbstractControl | null and discards everything the compiler knew. And put validation messages in the template next to the field, associated with aria-describedby — which leads directly to the next section.

8. Rendering: SSR, hydration and Core Web Vitals

Angular's server-side rendering story is genuinely competitive now, and for content-driven pages it is not optional. Three settings do most of the work.

providers: [
  provideClientHydration(
    withEventReplay(),          // clicks during hydration are not lost
    withIncrementalHydration(), // hydrate @defer blocks on demand
  ),
]

Event replay records interactions that happen before the JavaScript is ready and replays them after hydration, which removes the classic "I clicked and nothing happened" window. Incremental hydration goes further: a @defer (hydrate on viewport) block is rendered on the server, sent as HTML, and only ever hydrated if the user scrolls to it. For a long marketing or documentation page, that can mean shipping a fraction of the interactive JavaScript you used to.

For fully static routes, prerender instead — outputMode: 'static' with route parameters resolved at build time gives you one HTML file per URL, which is the cheapest and most crawlable thing you can serve. This site is built exactly that way. If you are weighing Angular SSR against the alternatives for a content-first product, I compared the two ecosystems in Journey from the World of Angular to Next.js.

9. Accessibility is a requirement, not a phase

For anyone shipping to the European market this stopped being a matter of craft in June 2025, when the European Accessibility Act began to apply to new consumer-facing digital products and services. The harmonised standard behind it, EN 301 549, tracks WCAG 2.1 level AA. In practice that means accessibility is now a procurement and compliance question, and it will be asked before your contract is signed rather than after your product ships.

The good news is that the majority of real-world failures come from a short list, and Angular makes all of them avoidable:

  • Semantic elements first. A <div> with a click handler is not a button. It has no keyboard behaviour, no role and no focus ring.
  • Visible focus everywhere. Never remove an outline without replacing it.
  • Labels and error association. Every input needs a real <label>; every error needs aria-describedby and aria-invalid.
  • Contrast in both themes. Dark mode is where AA ratios quietly break — verify the dark palette independently, not by eye.
  • Route changes announced. In a single-page application, navigation is silent to a screen reader unless you move focus to the new heading.
  • Respect prefers-reduced-motion for every non-essential animation.

Automated tooling catches perhaps a third of this. Put axe-core in your end-to-end suite so regressions fail the build, then test the primary flow with a keyboard only and with a screen reader once per release. That combination is inexpensive and it is what an audit will actually check.

10. A testing strategy worth the maintenance

Test volume is not the goal; test leverage is. The distribution I aim for:

LayerWhat to testTooling
Pure utilities Business rules, calculations, transforms — the highest-value tests you will write Jest, no TestBed
Stores State transitions and computed outputs, called as plain functions Jest, minimal TestBed
Components Rendered output and user interaction — not internal method calls Jest + Testing Library
Critical journeys Login, checkout, the one flow that costs money when it breaks Playwright

Because SignalStore state and computed values are just signals, most store tests need no asynchronous machinery at all: call the method, read the signal, assert. That is a large part of why moving logic out of components pays for itself so quickly — it converts slow, brittle component tests into fast, stable unit tests.

One warning worth stating plainly: do not assert on private methods or internal call counts. Those tests fail on every refactor and pass on every real bug, which is precisely backwards.

11. Keep the build honest

Performance regressions arrive one pull request at a time, so the only durable defence is an automated budget that fails CI.

"budgets": [
  { "type": "initial", "maximumWarning": "450kb", "maximumError": "550kb" },
  { "type": "anyComponentStyle", "maximumWarning": "4kb" }
]

Alongside that: keep the TypeScript strict family fully enabled (including noUncheckedIndexedAccess, which catches a genuine class of runtime error), run ng update on a schedule rather than in a panic every third year, and let Nx cache and affected-graph commands keep the pipeline fast as the repository grows. Tooling speed is also improving from underneath — I looked at what the Go port of the TypeScript compiler means for Angular build times in How Go is Transforming TypeScript.

The adoption order that works

If you are modernising an existing application rather than starting fresh, sequence matters more than speed. This order minimises risk because each step makes the next one safer:

  1. Standalone components. Run the official schematic, delete the NgModules.
  2. Built-in control flow. One schematic, mechanical, near-zero risk.
  3. OnPush everywhere plus signal-based component state.
  4. Business logic into stores and pure utilities, with unit tests as you go.
  5. Signal inputs and outputs in every component you touch for other reasons.
  6. @defer around the heaviest below-the-fold dependencies.
  7. Zoneless, once steps 3 to 5 are genuinely complete.
  8. SSR or prerendering with hydration for anything that needs to be found by search engines.

Do not attempt this as a rewrite. Every step on that list is independently shippable, which is the entire point — a modernisation that cannot be released in increments will be cancelled halfway through, and I have been called in to clean up several that were.

Closing thought

Modern Angular is a smaller framework than the one most teams learned. Less RxJS for state, no Zone.js, no NgModules, no manual change detection, less boilerplate around forms and stores. The craft has moved from knowing the framework's machinery to making good architectural decisions: where state lives, where boundaries fall, what gets deferred, what gets tested.

That is a much better problem to have, and it is why Angular remains my default for applications that need to be maintained by a team for years rather than demoed once. The framework finally rewards the things that were always true about good frontend engineering.

I am Sujit Yadav, an Angular, React and Next.js developer based in Nepal, working with teams worldwide. You can see the enterprise Angular applications I have built, or get in touch if you are modernising an Angular codebase and want a second opinion on the plan.