javascript

The Angular Signals Revolution: Rethinking Reactivity

Sonu Kapoor

Sonu Kapoor on

The Angular Signals Revolution: Rethinking Reactivity

In the world of Angular, reactivity has long been synonymous with RxJS. For years, developers have used observables, operators, and async pipes to build dynamic, reactive applications. But while powerful, RxJS brings a steep learning curve and often requires more mental juggling than necessary. Angular Signals change that. They introduce a new reactivity model that is intuitive, precise, and designed for clarity.

In this article, the first of a three-part series on Angular Signals, we won't rehash the official documentation. Instead, we'll take a fresh look at Signals from the perspective of someone building real-world apps, focusing on state management, performance, and debugging.

We'll compare Signals to RxJS and @Input() bindings, and demonstrate how Angular's new reactive primitives can simplify your codebase and make it easier to reason about.

Why Signals? Why Now?

Let’s start with a practical truth: while RxJS is powerful, it can be overkill for many UI use cases. Developers often reach for BehaviorSubject or combineLatest even when they just need a derived value from a couple of state variables. As applications grow, this can lead to convoluted pipelines that are difficult to trace and debug.

Signals solve this by embracing pull-based reactivity. Instead of setting up a stream and reacting to emissions, Signals allow you to read values reactively. They're synchronous, lazy, and transparent by design.

With Angular now embracing Signals as a first-class primitive, it’s time to reevaluate how we build reactive UIs.

RxJS vs. Signals: A Simpler Mental Model

RxJS follows a push-based model. Once a value changes, it pushes data through a pipeline of operators and subscribers. This can be powerful, but it requires careful management of subscriptions, teardown logic, and operator behavior.

Signals are pull-based. When you read a signal, it tracks what it depends on. If one of those dependencies changes, Angular marks it as dirty and re-evaluates it on the next reactive read. This subtle shift makes the mental model simpler: you look at the code and instantly know where the data is coming from and how it's used.

Let’s look at an example of a typical RxJS state setup:

ts
const count$ = new BehaviorSubject(0); const doubleCount$ = count$.pipe(map((x) => x * 2));

Now the same in Signals:

ts
const count = signal(0); const doubleCount = computed(() => count() * 2);

No pipes, no subscribe, and no teardown. Just declarative state.

Understanding the Core API: signal, computed, and effect

Angular provides three main primitives for signal-based reactivity:

  • signal(initialValue) - creates a writable reactive value.
  • computed(fn) - derives a new value from other signals.
  • effect(fn) - runs a side-effect whenever dependent signals change.

Let’s build a quick example that demonstrates all three:

ts
import { signal, computed, effect } from "@angular/core"; const price = signal(100); const quantity = signal(2); const total = computed(() => price() * quantity()); effect(() => { console.log(`Total changed to: ${total()}`); }); price.set(150); // Console: Total changed to: 300

Here, total reacts to changes in price or quantity. The effect responds whenever total() is accessed and its dependencies have changed. Everything is reactive, readable, and traceable.

This diagram illustrates the flow of reactivity in Angular’s Signals model:

Signals Workflow

A signal holds state and notifies its dependents when it changes. A computed value derives its result from one or more signals and automatically updates when any of them change. An effect listens to both signal and computed values and runs side effects — such as logging or UI updates — in response to those changes. This unidirectional flow ensures data remains predictable, dependencies are explicit, and side effects are isolated.

Refactoring an @Input() Example with Signals

Consider a component that takes an input and does something when it changes:

ts
@Input() price!: number; ngOnChanges(changes: SimpleChanges) { if (changes['price']) { this.recalculateTotal(); } }

With the new input<T>, this becomes:

ts
price = input<number>(); readonly total = computed(() => this.price() * 2);

We no longer need to manually detect changes. Everything is declarative. This reduces boilerplate and helps remove bugs caused by missed or mistyped input change handlers. In many cases, you can even eliminate lifecycle hooks like ngOnChanges entirely. With input signals and computed(), state changes are automatically tracked, encouraging a more reactive and maintainable approach to component logic.

Want to watch the result in the console? Add an effect:

ts
constructor() { effect(() => { console.log("Total updated:", this.total()); }); }

Debuggability and Observability Gains with Signals

Signals reduce what I call the “black box problem” of RxJS. In large codebases, you often don’t know what is emitting where, or what subscriptions exist deep in the component tree. This can lead to memory leaks, race conditions, and debugging nightmares.

Signals fix this by:

  • Being synchronous: You always know when a value changes.
  • Being traceable: You can follow the dependency chain by reading the code.
  • Having no teardown logic: There are no lingering subscriptions to clean up.

When used inside services and exposed as readonly signals, they give you a one-directional data flow that is easy to observe and test.

Here's an example from a service:

ts
@Injectable({ providedIn: "root" }) export class CartService { private readonly _items = signal<CartItem[]>([]); readonly items = this._items.asReadonly(); readonly total = computed(() => this._items().reduce((sum, item) => sum + item.price * item.qty, 0) ); addItem(item: CartItem) { this._items.update((items) => [...items, item]); } }

Here, total is always up to date, and no one outside the service can mutate the state directly. This creates clean boundaries and makes both debugging and testing much easier.

Signals: A Better Default for Angular Reactivity

Signals aren’t here to replace RxJS entirely. You’ll still use observables for async streams like WebSockets or router events. But for local UI state, they’re quickly becoming the better default.

However, Signals is catching up to RxJS when it comes to async streams. Angular now provides a new resource() function, which allows HTTP calls to integrate seamlessly with the Signals model. You can now fetch data reactively based on signal inputs, and Angular handles the caching, deduplication, and state management under the hood. This closes the gap between local and async state — without the need to reach for RxJS unless it's truly necessary.

In terms of performance, Signals pair beautifully with ChangeDetectionStrategy.OnPush. Since Signals are synchronous and dependency-tracked, Angular can skip entire component subtrees unless a relevant signal changes. This dramatically reduces unnecessary template re-evaluations and boosts runtime efficiency, especially in large applications.

Wrapping Up

In this post, we've seen how Signals help you:

  • Write less boilerplate
  • Reduce state-related bugs
  • Improve performance by reducing unnecessary change detection
  • Gain clearer, more predictable data flow

And most importantly: they make your code more readable and maintainable.

In our next article, we’ll apply these ideas to a real-world shopping cart and explore how to architect scalable, signal-driven state with services and computed values.

Angular is evolving. Signals make reactivity simpler, more predictable, and finally enjoyable again.

See you in the next one!

Wondering what you can do next?

Finished this article? Here are a few more things you can do:

  • Share this article on social media
Sonu Kapoor

Sonu Kapoor

Guest author Sonu Kapoor is a seasoned Senior Software Engineer and internationally recognized speaker with over two decades of experience in building high-performance web applications. He is a Microsoft MVP (2005–2010, 2024) and a Google Developer Expert for Angular, as well as a trusted contributor to the Angular Framework, where he has helped shape the future of frontend development. Besides that, he has published two books.

All articles by Sonu Kapoor

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps