DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Angular Signals in Practice — A Scientific, Production‑Minded Guide (2026)

Angular Signals in Practice — A Scientific, Production‑Minded Guide (2026)

Angular Signals in Practice — A Scientific, Production‑Minded Guide (2026)

Abstract

Angular Signals are no longer an experimental abstraction or a syntactic convenience — they are the foundational reactive primitive of modern Angular. This document presents a scientific, production‑oriented analysis of Signals, grounded in empirical comparison with RxJS, architectural reasoning, and real‑world use cases.

Rather than evangelizing Signals, we evaluate them.

We observe behavior, isolate variables, compare reactive models, and derive actionable conclusions for engineers building large‑scale Angular applications.


Why Signals Exist (Historical Context)

Angular’s original reactivity model was built around:

  • Zone.js
  • Global change detection
  • Imperative mutation + dirty checking

RxJS filled critical gaps:

  • Asynchronous streams
  • Event composition
  • Cancellation semantics

However, RxJS was never designed to be a UI state primitive. Over time, it became one by necessity, not intent.

Signals address this mismatch.

They are:

  • Synchronous
  • Deterministic
  • Dependency‑tracked
  • Pull‑based

This makes them ideal for state, not streams.


Mental Model: Signals vs Observables

Dimension Signals RxJS
Execution Pull Push
Timing Synchronous Async
Dependency Tracking Automatic Manual
UI State Native Incidental
Cancellation
Composition computed pipe
Lifecycle Scoped Subscription‑based

Key Insight: Signals are state machines. RxJS is event algebra.


Case Study: Idle User Logout (Insurance Domain)

Problem Definition

  • Users must be logged out after inactivity
  • A warning modal appears before logout
  • UI updates continuously as time elapses

This is derived UI state, not async orchestration.


RxJS Baseline (BehaviorSubject)

private idleTimeRemaining$ = new BehaviorSubject<number | null>(null);

idleLabel$ = this.idleTimeRemaining$.pipe(
  filter(Boolean),
  map(t => t! > 60 ? `${Math.ceil(t!/60)} minutes` : `${t} seconds`)
);
Enter fullscreen mode Exit fullscreen mode

Observations

  • Requires manual subscription management
  • UI consumes derived async state
  • Implicit lifecycle coupling
  • Overhead disproportionate to complexity

Signal‑Based Refactor

private idleTime = signal<number | null>(null);

idleLabel = computed(() => {
  const t = idleTime();
  if (t === null) return '';
  return t > 60 ? `${Math.ceil(t/60)} minutes` : `${t} seconds`;
});
Enter fullscreen mode Exit fullscreen mode

Observations

  • No subscriptions
  • Pure derivation
  • Deterministic recomputation
  • Explicit state ownership

Scientific Comparison

Variable Isolation

  • Same input events
  • Same UI
  • Same business logic

Only reactivity model changed.

Result

Signals reduced:

  • Cognitive load
  • Boilerplate
  • Failure modes

Without sacrificing correctness.


Signals as Deterministic State Machines

A signal represents:

A value + a dependency graph + a recomputation contract

Properties:

  • No hidden side effects
  • Referential transparency (when used correctly)
  • Cache invalidation handled by runtime

This enables local reasoning — a critical property in large systems.


Computed Signals: Rules of Physics

Computed signals must obey:

  1. Purity — no side effects
  2. Determinism — same inputs → same output
  3. No async — defer async to RxJS

Violating these rules breaks the model.

Use effect() for:

  • Logging
  • Persistence
  • DOM escape hatches

Dynamic Dependency Tracking

Signals track dependencies per execution, not per declaration.

const display = computed(() => {
  if (expanded()) return details();
  return summary();
});
Enter fullscreen mode Exit fullscreen mode

This enables:

  • Conditional reactivity
  • Efficient recomputation
  • Fine‑grained invalidation

Mutation Rules (Arrays & Objects)

Signals rely on structural change.

❌ Mutating objects does not trigger updates.

user().age = 31; // ❌
Enter fullscreen mode Exit fullscreen mode

✅ Replace immutably.

user.set({ ...user(), age: 31 });
Enter fullscreen mode Exit fullscreen mode

This enforces architectural discipline.


Equality Semantics

Signals use reference equality by default.

You may override equality for domain objects:

signal(user, { equal: (a,b) => a.id === b.id });
Enter fullscreen mode Exit fullscreen mode

Use sparingly. Incorrect equality functions cause stale state.


Effects: Controlled Side Effects

effect(() => {
  saveToStorage(state());
});
Enter fullscreen mode Exit fullscreen mode

Rules:

  • Never write to the same signal inside an effect
  • Avoid circular dependencies
  • Prefer effects in services

Signals + RxJS: A Truce

Signals do not replace RxJS.

Use RxJS for:

  • HTTP
  • WebSockets
  • User events
  • Cancellation
  • Complex async workflows

Bridge with:

  • toSignal()
  • toObservable()
  • rxResource()

This is the modern Angular stack.


Production Architecture Guidance

Use Signals For

  • Component state
  • View models
  • Derived UI
  • Feature‑local state

Use RxJS For

  • Data fetching
  • Side effects
  • Streams
  • Inter‑process communication

Anti‑Patterns

  • Signals for HTTP
  • Effects for derivation
  • Observables for toggles

Why This Matters in 2025+

Angular is moving toward:

  • Zoneless execution
  • Deterministic rendering
  • Fine‑grained invalidation

Signals are the cornerstone.

Ignoring them means fighting the framework.


Conclusion

Signals are not syntactic sugar.

They are a new execution model.

Used correctly, they:

  • Reduce bugs
  • Improve performance
  • Clarify intent
  • Scale better

RxJS remains essential — but no longer carries the burden of UI state.

This separation of concerns is Angular’s biggest architectural leap since Ivy.


✍️ Cristian Sifuentes

Full‑stack engineer • Angular & Reactive Systems

Top comments (0)