How Angular Signals Work — and Why It's Replacing RxJS in Many Cases

In this article we are going to discuss about how signal works in angular and why it's is replacing RxJs in many cases.

So if you've worked with RxJS in Angular, you know how it works. You create a BehaviorSubject, you call .next() to push new values then you subscribe in the component or use the async pipe in the template, and then you hope you didn't forget to unsubscribe somewhere or you'll have a memory leak. For fetching HTTP data or handling streams, RxJS is genuinely good. But for tracking a simple counter, a loading flag, or a toggle — it's too much setup for too little problem.

That's exactly what Signals are trying to fix.

This tutorial shows how to:

  • Understand what a Signal actually is
  • Create and read a Signal in a component
  • Use computed() for values that depend on other Signals
  • Use effect() to run code when a Signal changes
  • Know when Signals make sense and when to keep using RxJS

Why Angular Even Needed Signals

Angular has always relied on Zone.js for change detection. Zone.js basically patches browser APIs and tells Angular "something happened, go check everything." It works, but it's not very efficient — Angular ends up checking components that didn't even change.

RxJS helped with this to some extent, but it was never designed specifically for Angular's UI reactivity. It's a general-purpose reactive programming library. Using it for simple UI state feels like bringing a bulldozer to plant a flower.

Signals give Angular a proper reactive system built specifically for tracking UI state. When a Signal's value changes, Angular knows exactly which parts of the template depend on that Signal and updates only those parts. Nothing extra, nothing wasted.

And honestly, the code just reads better. Once you see it, you won't want to go back to BehaviorSubject for simple state.


Step 1: Create Your First Signal

No extra packages needed. Signals are part of @angular/core from Angular 16 onwards. Just import signal and you're good.

Here's a basic counter — probably the simplest example to start with:

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <h2>Count: {{ count() }}</h2>
    <button (click)="increment()">+</button>
    <button (click)="decrement()">-</button>
    <button (click)="reset()">Reset</button>
  `
})
export class CounterComponent {
  count = signal(0);

  increment() {
    this.count.update(val => val + 1);
  }

  decrement() {
    this.count.update(val => val - 1);
  }

  reset() {
    this.count.set(0);
  }
}

A couple of things to notice here. Reading a Signal in the template is done by calling it like a function — count(), not count. That's because a Signal is technically a function that returns its current value. Angular intercepts that call and tracks the dependency.

To update the value you have two options — set() and update(). More on that in the next step.


Step 2: set() vs update() — Which One to Use

This confuses a lot of people at first so let's clear it up.

set() is for when you already know the new value and it has nothing to do with the old one:

username = signal('guest');

login() {
  this.username.set('amit');
}

update() is for when the new value is based on whatever the current value is:

likes = signal(0);

addLike() {
  this.likes.update(current => current + 1);
}

Using set() in the second case would be wrong because you'd have to read the Signal separately, calculate the new value, then set it — which is messy. update() gives you the current value as an argument so you can work with it directly.

Both of them trigger Angular's change detection automatically. No manual detectChanges(), no markForCheck() — Angular just handles it.


Step 3: computed() — Signals That Depend on Other Signals

computed() is one of my favourite parts of the Signals API. It lets you create a Signal whose value is automatically calculated from one or more other Signals.

Say you're building a shopping cart. You have the number of items and the price per item as separate Signals. The total price should update on its own whenever either of those changes:

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

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    <p>Items: {{ itemCount() }}</p>
    <p>Price per item: ₹{{ pricePerItem() }}</p>
    <p><strong>Total: ₹{{ totalPrice() }}</strong></p>
    <button (click)="addItem()">Add Item</button>
  `
})
export class CartComponent {
  itemCount = signal(1);
  pricePerItem = signal(499);

  totalPrice = computed(() => this.itemCount() * this.pricePerItem());

  addItem() {
    this.itemCount.update(n => n + 1);
  }
}

You don't call totalPrice to update it. You don't subscribe to anything. Angular figures out that totalPrice depends on itemCount and pricePerItem, and whenever either changes, it recalculates totalPrice automatically.

In RxJS you'd be reaching for combineLatest here. This is simpler.


Step 4: effect() — Run Code When a Signal Changes

Sometimes you need to do something outside the template when a Signal changes — save to localStorage, log something, call a service. That's what effect() is for.

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

@Component({
  selector: 'app-theme',
  standalone: true,
  template: `
    <p>Theme: {{ theme() }}</p>
    <button (click)="toggle()">Toggle Theme</button>
  `
})
export class ThemeComponent {
  theme = signal('light');

  constructor() {
    effect(() => {
      document.body.setAttribute('data-theme', this.theme());
      localStorage.setItem('app-theme', this.theme());
    });
  }

  toggle() {
    this.theme.update(t => t === 'light' ? 'dark' : 'light');
  }
}

The effect() runs once when the component initializes, and then again every time theme changes. Angular automatically cleans up the effect when the component is destroyed, so no manual unsubscribe needed.

This is much cleaner than subscribing to a BehaviorSubject in ngOnInit and unsubscribing in ngOnDestroy.


Step 5: Signal-based Input in Angular 17+

Angular 17 added input() as a Signal-friendly replacement for @Input(). If you're building standalone components, this fits in really well.

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

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ fullName() }}</h3>
    </div>
  `
})
export class UserCardComponent {
  firstName = input<string>('');
  lastName = input<string>('');

  fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}

Parent uses it the same way as before:

<app-user-card firstName="Amit" lastName="Gautam" />

But now inside the child, firstName and lastName are Signals. You can plug them straight into computed() or effect() without any extra wiring.


Signals vs RxJS — The Honest Answer

People keep asking whether Signals will fully replace RxJS. The short answer is no, at least not anytime soon. But they will replace RxJS in a lot of places where RxJS didn't really belong.

Use Signals for local component state — counters, loading flags, form field values, toggles, selected tab, anything that lives inside a single component or is passed down as an input.

Keep RxJS for HTTP calls, WebSocket streams, debounced search inputs, retrying failed requests, or anything where you're dealing with real async event streams over time. HttpClient returns Observables and that's not changing.

The nice thing is you can mix both. Angular ships a utility called toSignal() that converts an Observable into a Signal:

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-users',
  standalone: true,
  template: `
    <ul>
      @for (user of users(); track user.id) {
        <li>{{ user.name }}</li>
      }
    </ul>
  `
})
export class UsersComponent {
  private http = inject(HttpClient);

  users = toSignal(
    this.http.get<any[]>('/api/users'),
    { initialValue: [] }
  );
}

No async pipe, no subscribe, no unsubscribe. The Observable from HttpClient feeds the Signal, and the template reads the Signal. Clean and simple.


Summary

You learned what Angular Signals are and how to use them in real components. You covered:

  • What a Signal is and why Angular introduced it
  • Creating a Signal with signal() and reading it in templates using count()
  • Updating Signal values with set() and update()
  • Creating auto-calculated values with computed()
  • Running side effects with effect() and why it beats manual subscriptions
  • Using input() for Signal-based component inputs in Angular 17+
  • When to use Signals and when to stick with RxJS
  • Bridging the two with toSignal() for HTTP data

Signals won't replace every single piece of RxJS in your app. But for managing what's happening inside your components, they're genuinely better — simpler to write, easier to read, and no subscription management headaches.

Give it a try in your next component and see how it feels.

I hope you like this article...

Happy coding! 🚀

Post a Comment

0 Comments