← Back to Articles         
RxJS-based state vs Signals — when should each be used?

RxJS-based state vs Signals — when should each be used?

Published on 02 Oct 2026     11 min read Angular
Older Angular vs Modern Angular

RxJS-Based State vs Signals — When Should Each Be Used?

Both RxJS and Signals can manage state in Angular, but they are optimized for different kinds of problems.

A useful starting point is:

Signals are excellent for synchronous application/UI state. RxJS is excellent for asynchronous event streams and complex async workflows.

In real Angular applications, they are often used together, not treated as mutually exclusive alternatives.


1. What Is State?

State is data that can change while the application is running and that the UI or application logic depends on.

Examples:

Logged-in user
Selected product
Shopping cart
Search text
Loading status
Products returned from API
Current page
Selected category
Notifications

For example:

products = ...
loading = ...
selectedCategory = ...

These values represent application state.


2. What Is Signal-Based State?

A Signal is Angular's reactive primitive for storing a value and notifying consumers when that value changes.

Example:

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

const count = signal(0);

Read:

console.log(count());

Update:

count.set(10);

or:

count.update(value => value + 1);

Full Simple Signal State Example

Suppose we want to maintain a product list.

product.store.ts

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

export interface Product {
  id: number;
  name: string;
  price: number;
}

@Injectable({
  providedIn: 'root'
})
export class ProductStore {

  private readonly _products = signal<Product[]>([]);

  private readonly _loading = signal(false);

  readonly products = this._products.asReadonly();

  readonly loading = this._loading.asReadonly();

  readonly productCount = computed(
    () => this._products().length
  );

  setProducts(products: Product[]) {

    this._products.set(products);

  }

  addProduct(product: Product) {

    this._products.update(products => [
      ...products,
      product
    ]);

  }

  setLoading(value: boolean) {

    this._loading.set(value);

  }
}

We now have state:

_products
_loading

and derived state:

productCount

Component

import { Component, inject } from '@angular/core';
import { ProductStore } from './product.store';

@Component({
  selector: 'app-products',

  template: `
    <h2>Products</h2>

    <p>Total: {{ store.productCount() }}</p>

    @if (store.loading()) {

      <p>Loading...</p>

    }

    @for (product of store.products(); track product.id) {

      <p>
        {{ product.name }}
        -
        {{ product.price }}
      </p>

    }
  `
})
export class Products {

  readonly store = inject(ProductStore);

}

Notice how simple template consumption is:

store.products()

and:

store.loading()

No manual subscribe() is necessary.


Signal Flow

ProductStore
     ↓
signal<Product[]>()
     ↓
products()
     ↓
Component template
     ↓
Signal changes
     ↓
Angular updates affected UI

3. What Is RxJS-Based State?

RxJS manages reactive data using:

Observable
Subject
BehaviorSubject
Operators

For state management, BehaviorSubject has traditionally been very common because it:

  • Holds a current value
  • Emits the current value to new subscribers
  • Emits new values whenever the state changes

Full Simple RxJS State Example

product.store.ts

import { Injectable } from '@angular/core';

import {
  BehaviorSubject
} from 'rxjs';

import {
  map
} from 'rxjs/operators';

export interface Product {
  id: number;
  name: string;
  price: number;
}

@Injectable({
  providedIn: 'root'
})
export class ProductStore {

  private readonly productsSubject =
    new BehaviorSubject<Product[]>([]);

  readonly products$ =
    this.productsSubject.asObservable();

  readonly productCount$ =
    this.products$.pipe(
      map(products => products.length)
    );

  setProducts(products: Product[]) {

    this.productsSubject.next(products);

  }

  addProduct(product: Product) {

    const currentProducts =
      this.productsSubject.value;

    this.productsSubject.next([
      ...currentProducts,
      product
    ]);

  }

}

Component

import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';

import {
  ProductStore
} from './product.store';

@Component({
  selector: 'app-products',

  imports: [
    AsyncPipe
  ],

  template: `
    <h2>Products</h2>

    <p>
      Total:
      {{ store.productCount$ | async }}
    </p>

    @for (
      product of store.products$ | async;
      track product.id
    ) {

      <p>
        {{ product.name }}
        -
        {{ product.price }}
      </p>

    }
  `
})
export class Products {

  readonly store = inject(ProductStore);

}

Here:

products$

is an Observable.

The template subscribes using:

products$ | async

4. Main Difference

Think of them this way:

SIGNAL
======

"I have a value.
Tell Angular when this value changes."


RXJS
====

"I have a stream of values/events over time.
I need to transform, combine, cancel,
filter or coordinate those events."

That's the fundamental conceptual difference.


5. When Should Signals Be Used?

Signals are particularly suitable for synchronous state that Angular UI depends on.

Examples:

Current selected tab
Sidebar open/closed
Selected category
Search filter
Current user state
Product list already loaded
Loading flag
Current page number
Form-related UI state
Shopping cart state

Example:

selectedCategoryId = signal<number | null>(null);

currentPage = signal(1);

loading = signal(false);

products = signal<Product[]>([]);

These are straightforward state values.


6. Signals Are Excellent for Derived State

Suppose:

products = signal<Product[]>([
  { id: 1, name: 'Laptop', price: 50000 },
  { id: 2, name: 'Mouse', price: 1000 }
]);

We can derive:

expensiveProducts = computed(() =>
  this.products()
      .filter(p => p.price > 10000)
);

Then:

products
    ↓
computed()
    ↓
expensiveProducts

When products changes, Angular recalculates the derived value when necessary.

This is a natural use case for Signals.


7. When Should RxJS Be Used?

RxJS is particularly powerful when dealing with asynchronous streams over time.

Examples include:

  • HTTP requests
  • Search/autocomplete
  • Debouncing
  • WebSocket streams
  • Multiple dependent API calls
  • Combining asynchronous sources
  • Cancellation
  • Retry logic
  • Event streams
  • Complex async orchestration

Example: Search Box

Imagine the user types:

L
La
Lap
Lapt
Lapto
Laptop

You don't want six API requests.

You may want:

User types
    ↓
Wait 300 ms
    ↓
Ignore duplicate value
    ↓
Cancel previous API request
    ↓
Call API using latest value

RxJS is excellent for this.


RxJS Search Example

searchControl.valueChanges
  .pipe(

    debounceTime(300),

    distinctUntilChanged(),

    switchMap(search =>
      this.productService.search(search)
    )

  )
  .subscribe(products => {

    console.log(products);

  });

Here RxJS operators solve several async problems elegantly:

valueChanges
      ↓
debounceTime()
      ↓
distinctUntilChanged()
      ↓
switchMap()
      ↓
HTTP API

Signals alone are not intended to replace the full RxJS operator model for this kind of asynchronous stream orchestration.


8. Signals vs RxJS Example

Suppose your Products page has:

Search text
Selected category
Current page
Products
HTTP API

A practical design could use:

Signals for state

search = signal('');

categoryId = signal<number | null>(null);

page = signal(1);

products = signal<Product[]>([]);

loading = signal(false);

RxJS for HTTP workflow

this.http
  .get<Product[]>('/api/products')
  .subscribe(products => {

    this.products.set(products);

  });

So:

ASP.NET Core API
      ↓
HttpClient
      ↓
Observable / RxJS
      ↓
API result
      ↓
Signal state
      ↓
Angular template

This combination is very common and useful.


9. More Realistic Angular + ASP.NET Core Example

Suppose Angular calls:

GET /api/products?search=laptop

product.service.ts

import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class ProductService {

  private readonly http = inject(HttpClient);

  searchProducts(search: string) {

    return this.http.get<Product[]>(
      '/api/products',
      {
        params: {
          search
        }
      }
    );

  }

}

The service returns an:

Observable<Product[]>

because HttpClient naturally works with RxJS.


Component State With Signals

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

@Component({
  selector: 'app-products',

  template: `
    @if (loading()) {
      <p>Loading...</p>
    }

    @for (product of products(); track product.id) {

      <p>{{ product.name }}</p>

    }
  `
})
export class Products {

  private readonly productService =
    inject(ProductService);

  readonly products =
    signal<Product[]>([]);

  readonly loading =
    signal(false);

  loadProducts() {

    this.loading.set(true);

    this.productService
      .searchProducts('laptop')
      .subscribe({

        next: products => {

          this.products.set(products);

          this.loading.set(false);

        },

        error: error => {

          console.error(error);

          this.loading.set(false);

        }

      });

  }

}

Here:

RxJS
 ↓
handles HTTP Observable


Signal
 ↓
stores UI state

10. Signals and RxJS Can Be Converted

Angular provides RxJS interoperability utilities.

For example:

toSignal()

can turn an Observable into a Signal.


Observable → Signal

Suppose:

products$ =
  this.productService.getProducts();

We can write:

import { toSignal }
  from '@angular/core/rxjs-interop';

products = toSignal(
  this.productService.getProducts(),
  {
    initialValue: []
  }
);

Now the template can use:

@for (product of products(); track product.id) {

  <p>{{ product.name }}</p>

}

instead of:

@for (product of products$ | async; track product.id) {

Conceptually:

HTTP
 ↓
Observable<Product[]>
 ↓
toSignal()
 ↓
Signal<Product[]>
 ↓
Template

11. Signal → Observable

Angular also provides:

toObservable()

Example:

search = signal('');

search$ = toObservable(this.search);

Now RxJS operators can be applied:

this.search$
  .pipe(

    debounceTime(300),

    distinctUntilChanged(),

    switchMap(search =>
      this.productService.searchProducts(search)
    )

  );

This gives a powerful combination:

Signal
  ↓
toObservable()
  ↓
RxJS operators
  ↓
HTTP

12. Practical Search Example Using Both

Suppose:

search = signal('');

The UI changes the signal.

Convert it:

search$ = toObservable(this.search);

Then:

products = toSignal(

  this.search$.pipe(

    debounceTime(300),

    distinctUntilChanged(),

    switchMap(search =>
      this.productService.searchProducts(search)
    )

  ),

  {
    initialValue: []
  }

);

The complete flow becomes:

User types
   ↓
search Signal
   ↓
toObservable()
   ↓
debounceTime(300)
   ↓
distinctUntilChanged()
   ↓
switchMap()
   ↓
ASP.NET Core API
   ↓
Observable<Product[]>
   ↓
toSignal()
   ↓
products Signal
   ↓
Template

This demonstrates why the question should not usually be:

"Should I use RxJS or Signals?"

A better question is:

"Which part of this problem is state, and which part is an asynchronous stream?"


13. Signals vs RxJS Comparison

Feature Signals RxJS
Primary concept Reactive value/state Stream of values/events
Synchronous state Excellent Possible
UI state Excellent Good
Derived state computed() Operators
Side effects effect() tap(), subscription
HTTP Can consume/bridge Excellent
Debouncing Not primary strength Excellent
Cancellation Limited as a state primitive Excellent
Multiple async streams Less direct Excellent
WebSockets Not primary tool Excellent
Stream transformation Limited Extensive
Learning complexity Lower Higher
Template usage value() observable$ | async
Current value Naturally available Depends on stream type
Angular change detection integration Excellent Supported

14. When Should I Choose Signals?

Choose Signals when your main requirement sounds like:

"I need to store a value, update it, derive other values from it, and automatically update Angular's UI."

For example:

products = signal<Product[]>([]);

selectedProduct = signal<Product | null>(null);

loading = signal(false);

currentPage = signal(1);

pageSize = signal(10);

Derived:

totalProducts = computed(
  () => this.products().length
);

Signals are particularly natural for component and UI-oriented state.


15. When Should I Choose RxJS?

Choose RxJS when your requirement sounds like:

"I have events or asynchronous values occurring over time, and I need to transform or coordinate them."

For example:

Search input
   ↓
debounce
   ↓
cancel previous request
   ↓
new API request

RxJS:

search$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(search =>
    this.productService.searchProducts(search)
  )
);

Another example:

API 1
API 2
API 3
 ↓
Wait until all complete

RxJS:

forkJoin({
  products: this.productService.getProducts(),
  categories: this.categoryService.getCategories(),
  tags: this.tagService.getTags()
});

This is where RxJS remains extremely valuable.


16. A Simple Decision Rule

Ask yourself:

Do I primarily have a value/state?

Selected category
Current user
Products list
Loading status
Page number
Sidebar state

Use:

Signal

Do I primarily have an async/event stream?

HTTP requests
WebSocket events
Search typing
Multiple APIs
Debouncing
Cancellation
Retry
Complex async sequencing

Use:

RxJS

Do I have both?

Use:

Signals + RxJS

17. Example for a Typical Angular + ASP.NET Core Application

For an Angular frontend calling ASP.NET Core Web API, a practical architecture is:

Angular Component
       │
       ├── Signals
       │     ├── products
       │     ├── loading
       │     ├── selectedCategory
       │     └── currentPage
       │
       ↓
Angular Service
       │
       ├── HttpClient
       │
       ↓
     RxJS
       │
       ├── switchMap
       ├── catchError
       ├── retry
       └── other async operators
       │
       ↓
ASP.NET Core Web API
       ↓
Application Layer
       ↓
Database

That is often a cleaner mental model than trying to force everything into either Signals or RxJS.


Key Points

  • Signals and RxJS solve overlapping but different problems.
  • Signals are excellent for synchronous reactive state.
  • RxJS is excellent for asynchronous streams and event orchestration.
  • Signals use:
signal()
computed()
effect()
  • RxJS uses concepts such as:
Observable
Subject
BehaviorSubject

and operators such as:

map()
filter()
switchMap()
debounceTime()
catchError()
forkJoin()
combineLatest()
  • Angular HttpClient works naturally with Observables.
  • Signals don't make RxJS obsolete.
  • You can combine them using:
toSignal()
toObservable()

A strong practical rule is:

STATE
  ↓
Signals


ASYNC STREAM
  ↓
RxJS


REAL APPLICATION
  ↓
Often Signals + RxJS

Interview Questions and Answers

1. What is the main difference between Signals and RxJS?

Signals primarily represent reactive values/state, while RxJS Observables represent streams of values or events over time.


2. When should Signals be used?

Signals are well suited for local/component/application UI state such as:

loading = signal(false);

products = signal<Product[]>([]);

selectedCategory = signal<number | null>(null);

3. When should RxJS be used?

RxJS is useful for complex asynchronous workflows such as HTTP requests, debouncing, cancellation, WebSockets and combining multiple streams.

Example:

search$.pipe(
  debounceTime(300),
  switchMap(search =>
    this.service.search(search)
  )
);

4. Do Signals replace RxJS?

No.

Signals reduce the need to use RxJS for some straightforward state-management scenarios, but RxJS remains highly useful for asynchronous and event-stream processing.


5. Should BehaviorSubject always be replaced with signal()?

No.

If a BehaviorSubject is primarily being used as a simple state container, a Signal may provide a simpler solution.

If the state participates heavily in RxJS stream composition, keeping it as an Observable/Subject-based design can make sense.


6. How do you convert an Observable to a Signal?

Using:

toSignal()

Example:

products = toSignal(
  this.service.getProducts(),
  {
    initialValue: []
  }
);

7. How do you convert a Signal to an Observable?

Using:

toObservable()

Example:

search = signal('');

search$ = toObservable(this.search);

8. Why is RxJS useful for search?

Because operators such as:

debounceTime()
distinctUntilChanged()
switchMap()

make it easy to debounce user input and switch to the latest API request.


9. Can Signals and RxJS be used together?

Yes. This is often a strong design.

For example:

Search Signal
     ↓
toObservable()
     ↓
RxJS switchMap()
     ↓
HTTP API
     ↓
toSignal()
     ↓
Products Signal
     ↓
Template

10. For an Angular + ASP.NET Core application, which should I use?

A common approach is:

  • Signals for Angular UI/application state.
  • RxJS for HttpClient and complex asynchronous workflows.
  • Use toSignal() / toObservable() when crossing between the two models.

Interview Quick Answer

Use Signals primarily for synchronous reactive UI/application state and derived state. Use RxJS when dealing with asynchronous streams that require operations such as debouncing, cancellation, retries, or combining multiple streams. They are complementary: modern Angular applications often use Signals for state and RxJS for asynchronous workflows.

← Back to Articles