Angular Signals vs RxJS: Where They Overlap, Where They Complement
Compare Angular Signals and RxJS for reactive state management. Learn where they overlap, where they complement each other, and how Angular's reactive model is evolving.
Angular Signals and RxJS are both reactive primitives, but they solve fundamentally different problems. Signals manage synchronous state and derived values. RxJS manages asynchronous event streams with operators for timing, cancellation, and orchestration. Understanding where they overlap and where they complement each other is essential for building Angular applications that are simple where possible and powerful where necessary.
What signals are designed for
Signals are Angular’s answer to synchronous reactive state. They represent a value that changes over time and automatically notify consumers when it updates.
The core signal APIs include:
signal()— writable reactive valuecomputed()— derived value that recalculates when dependencies changeeffect()— side effect that runs when signal dependencies changelinkedSignal()— dependent state that resets when a source signal changesresource()— declarative async data fetching tied to signal parameters
Signals are pull-based: the template or a computed function reads the current value when it needs it. There is no subscription management, no async pipe, and no manual teardown. Change detection in Angular uses signals to know exactly which components need to re-render.
const count = signal(0);
const doubled = computed(() => count() * 2);
// Template reads doubled() — updates automatically when count changes
Signals excel at:
- Component-local UI state (toggles, form state, selected items)
- Derived values across multiple sources (filtered lists, totals, validation status)
- Replacing
BehaviorSubjectfor simple state holders - Template reactivity without
asyncpipe boilerplate
What RxJS is designed for
RxJS is a library for composing asynchronous and event-based programs using observable sequences. It provides operators for transforming, filtering, combining, and managing streams of events over time.
The core RxJS patterns in Angular include:
HttpClientreturns observables for HTTP requestsswitchMap/mergeMap/concatMapfor flattening async operationsdebounceTime/throttleTimefor rate limitingcombineLatest/forkJoinfor combining multiple async sourcesretry/catchErrorfor error recoverytakeUntilDestroyedfor lifecycle-aware subscriptionsSubject/BehaviorSubjectfor imperative event buses
RxJS is push-based: values arrive over time, and operators transform or react to them as they flow through the stream. This is ideal for scenarios where timing, ordering, and cancellation matter.
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.http.get<Result[]>(`/api/search?q=${term}`))
).subscribe(results => this.results.set(results));
RxJS excels at:
- HTTP request orchestration (cancellation, retry, sequential calls)
- User input streams (typeahead, drag events, scroll position)
- WebSocket and real-time data streams
- Complex async workflows (polling, race conditions, timeout handling)
- Event coordination across multiple sources with timing constraints
Where they overlap
There is a genuine overlap between signals and RxJS in a few areas:
Simple state holders
A BehaviorSubject and a signal() both hold a current value and notify consumers on change. For this use case, signals are strictly simpler:
// Before: RxJS
private readonly count$ = new BehaviorSubject(0);
readonly count$ = this.count$.asObservable();
// After: Signals
private readonly _count = signal(0);
readonly count = this._count.asReadonly();
No subscription needed, no async pipe in the template, no memory leak risk from forgotten unsubscribes.
Derived state
Both combineLatest with map and computed() can derive values from multiple sources:
// RxJS
readonly total$ = combineLatest([this.price$, this.quantity$]).pipe(
map(([price, qty]) => price * qty)
);
// Signals
readonly total = computed(() => this.price() * this.quantity());
For synchronous derivations, computed() is simpler and more efficient. It only recalculates when a dependency actually changes, and it integrates directly with Angular’s change detection.
Event reactions
Both effect() and tap() / subscribe() can trigger side effects in response to changes. However, effect() is limited to synchronous reactions to signal changes, while RxJS provides operators for debouncing, throttling, and async side effects.
Where they complement each other
The most effective Angular architecture uses both: RxJS for async orchestration and signals for state and template binding.
Pattern: RxJS fetches, signals store
The most common hybrid pattern is: use RxJS to manage the HTTP lifecycle (cancellation, retries, error handling), then write the result into a signal for the template:
@Injectable()
export class ArticleStore {
private readonly http = inject(HttpClient);
private readonly _articles = signal<Article[]>([]);
private readonly _loading = signal(false);
private readonly _error = signal<string | null>(null);
readonly articles = this._articles.asReadonly();
readonly loading = this._loading.asReadonly();
readonly error = this._error.asReadonly();
loadArticles(category: string) {
this._loading.set(true);
this._error.set(null);
this.http.get<Article[]>(`/api/articles?cat=${category}`).pipe(
retry({ count: 2, delay: 1000 }),
finalize(() => this._loading.set(false))
).subscribe({
next: articles => this._articles.set(articles),
error: err => this._error.set(err.message)
});
}
}
The template reads signals directly — no async pipe, no subscription tracking. RxJS handles what it is good at (HTTP lifecycle), and signals handle what they are good at (reactive state for the view).
Pattern: RxJS for user input, signals for state
For typeahead search, RxJS manages debounce and cancellation while signals hold the result:
@Component({...})
export class SearchComponent {
private readonly searchControl = new FormControl('');
private readonly http = inject(HttpClient);
readonly results = signal<SearchResult[]>([]);
constructor() {
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
filter(term => term.length >= 2),
switchMap(term => this.http.get<SearchResult[]>(`/api/search?q=${term}`)),
takeUntilDestroyed()
).subscribe(results => this.results.set(results));
}
}
Pattern: WebSocket streams into signals
Real-time data from a WebSocket is inherently stream-based. RxJS manages the connection and signals expose the latest state:
@Injectable({ providedIn: 'root' })
export class LivePriceService {
private readonly _prices = signal<Map<string, number>>(new Map());
readonly prices = this._prices.asReadonly();
connect() {
webSocket<PriceUpdate>('wss://prices.example.com').pipe(
retryWhen(errors => errors.pipe(delay(5000)))
).subscribe(update => {
this._prices.update(map => {
const next = new Map(map);
next.set(update.symbol, update.price);
return next;
});
});
}
}
How Angular’s reactive model is evolving
Angular’s roadmap shows a clear direction: signals as the primary reactive primitive for state and change detection, with RxJS remaining available for async workflows.
What has already shipped
- Signal-based components with
input(),output(),model(),viewChild(),contentChild() resource()andrxResource()for declarative async data loadinglinkedSignal()for dependent state managementtoSignal()andtoObservable()for interop between signals and observables- Zoneless change detection powered by signals
Where Angular is heading
- Less reliance on RxJS for common tasks:
resource()reduces the need for manual HTTP observable management. Many components will not need RxJS at all. - RxJS becomes opt-in, not mandatory: future Angular versions may reduce or make RxJS an optional peer dependency for teams that do not need it.
- Signal-based forms: the Angular team has discussed signal-based form primitives that would replace or complement
ReactiveFormsModule. - Signal-based router events: router navigation could expose signals instead of observables for common use cases.
What this means for your codebase
RxJS is not going away. It remains the best tool for complex async patterns. But the surface area where RxJS is necessary is shrinking. New Angular projects should default to signals for state and reach for RxJS only when the problem is genuinely async or stream-based.
Decision guide: when to use which
| Scenario | Use Signals | Use RxJS | Use Both |
|---|---|---|---|
| Component UI state (toggle, tab, selected item) | Yes | ||
| Derived values (totals, filters, validation) | Yes | ||
| Simple data fetch (load once, display) | Yes (resource()) | ||
| Search typeahead with debounce | Yes | ||
| HTTP with retry, cancellation, sequencing | Yes | ||
| WebSocket / real-time streams | Yes | ||
| Cross-feature event coordination | Yes | ||
| Form value tracking (reactive forms) | Yes (for now) | ||
| Route parameter reactions | Yes (input()) | ||
| Polling with interval and stop conditions | Yes |
Rule of thumb
If the value exists right now and you need to read it — use a signal. If the value will arrive over time and you need to transform, filter, or coordinate it — use RxJS. If both are true, use RxJS for the stream and write the result into a signal.
FAQ
Should I stop using RxJS in Angular?
No. RxJS remains essential for async orchestration, HTTP lifecycle management, and event stream processing. What you should stop doing is using BehaviorSubject as a state holder when a signal is simpler, and using combineLatest for derived state when computed() is cleaner.
Can I use signals without RxJS at all?
For many components, yes. With resource() for data fetching and signals for state, a component may have zero RxJS imports. However, at the service or infrastructure level, RxJS is still the right tool for complex async patterns.
Is toSignal() a good long-term pattern?
toSignal() is a pragmatic bridge for converting existing observables to signals. It works well for gradual migration. In new code, prefer writing to signals directly from subscription handlers rather than wrapping entire observable chains in toSignal(), since this gives you more control over error and loading states.
Will Angular remove RxJS as a dependency?
Angular may make RxJS an optional peer dependency in future versions, but it will not remove RxJS support. Teams that rely on observables for forms, HTTP, and router events will continue to use RxJS. The shift is toward making RxJS opt-in for projects that do not need it.
What should new Angular developers learn first?
Learn signals first. They are simpler, more intuitive, and cover the majority of reactive patterns in modern Angular. Then learn RxJS fundamentals (operators, subscription lifecycle, error handling) for when you encounter async orchestration challenges that signals cannot solve alone.
Conclusion and next steps
Signals and RxJS are not competitors — they are complementary tools for different reactive problems. Signals simplify synchronous state, derived values, and template binding. RxJS manages async streams, orchestration, and timing. The best Angular applications use both intentionally: signals as the default, RxJS when the problem demands it.
Audit one feature in your application. Identify every BehaviorSubject used as a simple state holder and every combineLatest used for synchronous derivation. These are candidates for migration to signals. For a deeper look at how to structure signal-based state in enterprise teams, read Angular Signals for Enterprise State Architecture. If you are also weighing whether to keep NgRx in your stack, see NgRx: To Be or Not to Be in Modern Angular.
Modern Angular Architecture