Feb 11, 2026 5 min read

Angular Signals in Production: From First Signal to a Signals-First Architecture

Signals started as an experiment in Angular 16. By now they're the center of gravity of the framework: signal inputs, model(), linkedSignal, resource APIs, and zoneless change detection all assume you think in signals. I've introduced them into large production Angular codebases – the kind with years of RxJS habits baked in – and this is the guide I wish existed: not just the API, but where signals genuinely change your architecture, where RxJS still wins, and how to migrate without a rewrite.

The mental model that matters

A signal is a value with a subscription list. That's it. The power is in what the framework does with that subscription list: when a signal used in a template changes, Angular knows exactly which views to update – no tree-walking, no zone patching every callback in the browser.

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

const cart = signal<CartItem[]>([]);
const total = computed(() =>
  cart().reduce((sum, item) => sum + item.price * item.qty, 0)
);

cart.update(items => [...items, newItem]); // total recomputes, views that read it update

Two properties are worth internalizing early:

  • Computeds are lazy and glitch-free. A computed doesn't run until something reads it, and a chain of computeds settles consistently – you never observe a half-updated intermediate state. This kills a whole category of RxJS combineLatest glitches.
  • Reads are tracked, writes are not. Whatever you read inside a computed or effect becomes a dependency. This is why hiding a read inside a helper function can silently create dependencies you didn't intend – keep reactive reads visible.

Components: signal inputs and model

The component API is where signals pay off first, because it deletes boilerplate you've been writing for years:

@Component({ /* ... */ })
export class PriceTag {
  price = input.required<number>();      // replaces @Input + ngOnChanges
  currency = input('USD');
  formatted = computed(() =>
    new Intl.NumberFormat('en', { style: 'currency', currency: this.currency() })
      .format(this.price())
  );
}

Everything derived from inputs becomes a computed instead of ngOnChanges bookkeeping. For two-way binding, model() replaces the @Input()/@Output() pair. Combined with ChangeDetectionStrategy.OnPush (or a zoneless app), templates update only when the exact signals they read change.

The rules for effect

effect is the sharpest knife in the drawer, and most signal-related messes I've cleaned up in reviews trace back to it. The rules I hold the line on:

  1. Effects are for leaving the reactive world – syncing to localStorage, imperative chart libraries, logging, analytics. If an effect writes to another signal, you almost always wanted computed or linkedSignal.
  2. Use untracked deliberately. Reading a signal you don't want as a dependency (say, a config value) inside an effect? Wrap it in untracked(() => config()) and say so in the code.
  3. One effect, one job. An effect with three unrelated reads reruns for all three reasons. Split it.

linkedSignal deserves a special mention: it's a writable signal that resets when a source changes – the "selected item resets when the list reloads" pattern that used to take a subject, an operator chain, and a bug report.

RxJS isn't dead – it's demoted

The honest architecture in 2026 is: signals for state, RxJS for events.

  • Anything that is "the current value of X" – form state, selected filters, loaded entities, UI flags – is a signal.
  • Anything that is "a stream of things happening over time" – websocket messages, drag events, typeahead with debounce and cancellation – stays RxJS. Signals have no notion of time; switchMap has no signal equivalent, and that's fine.

The bridge is two functions: toSignal(stream$) at the boundary where a stream becomes state, and toObservable(sig) in the rare opposite direction. In practice, my services expose signals outward and keep whatever RxJS they need as an implementation detail. Components stop injecting AsyncPipe-driven streams and read signals directly.

Migrating a large codebase without stopping

The strategy that worked on real teams, in order:

  1. New code is signals-first from day one. A lint-level convention, not a debate in every PR.
  2. Convert leaf components (presentational, input-driven) to signal inputs – mechanical, low-risk, and it builds the team's instincts.
  3. Introduce signal-based state services for each feature as you touch it: a signal for the raw state, computed for every derived view, methods for mutations. This replaces ad-hoc BehaviorSubject soup one feature at a time.
  4. NgRx stays until it doesn't. SignalStore is a natural landing spot if you rely on NgRx heavily, but don't migrate a working store for fashion – migrate when you're rewriting that feature anyway.
  5. Zoneless last. Removing zone.js is the payoff step, but only after the codebase no longer relies on zone-triggered change detection quirks.

That ordering matters because each step is independently shippable – the same principle I apply to any legacy migration: never a phase that blocks the roadmap.

Pitfalls that only show up at scale

  • Effects as a state-sync bus. Two effects writing each other's sources is an infinite loop waiting for the right data. Derive, don't sync.
  • Fat computeds. A computed that filters and sorts a 10k-row array reruns on every dependency change. Memoize the expensive part or restructure so the hot path depends on less.
  • Signals in services with request scope confusion. A signal in a root-provided service is global state – great, if that's what you meant. Audit who writes it.
  • Mutating signal contents. cart().push(item) changes the array without notifying anyone. Signals compare by reference; always update with a new reference.

Signals are the rare framework change that genuinely simplifies code as it scales – but the transition rewards teams that move deliberately. If your Angular platform is mid-transition (or hasn't started) and you want the migration to be boring, that's exactly the work I do – see frontend architecture & tech leadership or write to sinclar96@gmail.com.

← All posts

Building something in this stack?

Get in touch