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
HttpClientworks 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
HttpClientand 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.