Angular Signals vs RxJS: When to Use What
A practical decision framework for combining Angular Signals and RxJS without forcing either tool into the wrong job.
Angular Signals make local reactive state much easier to express, while RxJS remains excellent for asynchronous streams, cancellation, timing, and coordination. The mistake is treating this as a winner-takes-all decision. The strongest Angular applications use each tool where its model is clearest.
Start with the shape of the problem
Use a signal when state is local, synchronous, and primarily consumed by the template: a selected tab, a form mode, a loading flag, or a value derived from other local values. Signals are direct to read and write, and computed values make dependencies explicit.
readonly selectedTab = signal<'overview' | 'activity'>('overview');
readonly isActivityVisible = computed(() => this.selectedTab() === 'activity');
Use RxJS when the problem involves a sequence over time: HTTP requests triggered by user input, WebSocket events, route changes, retry logic, debounce behavior, or combining independent asynchronous sources.
readonly results$ = this.searchTerms.pipe(
debounceTime(250),
distinctUntilChanged(),
switchMap((term) => this.api.search(term))
);
The question is not “can I make this work with signals?” It is “which model makes cancellation, ownership, and failure behavior easiest to understand six months from now?”
Why signals work so well for UI state
Signals remove a lot of ceremony from component state. There is no subscription to manage for a value created within the component, and a template can read a signal directly. Computed values also prevent duplicated derived state.
Good signal candidates include:
- Open or closed UI controls.
- Selected IDs, sort direction, and filters.
- Local form interaction state.
- Derived display values.
- Simple loading and error flags owned by one feature.
Avoid storing the same fact in multiple writable signals. If a label is derived from a selected item, calculate it with computed() instead of updating it manually in several event handlers.
Why RxJS remains essential
Observables describe values that arrive over time. Their operators make temporal behavior visible: switchMap cancels stale work, concatMap preserves order, exhaustMap ignores repeated submissions, and retry describes recovery.
These are not edge cases. They are everyday application behavior. Consider a typeahead search: the latest term should cancel the previous request. With RxJS, the requirement is expressed directly with switchMap. Rebuilding that behavior with ad hoc effects usually creates more code and less clarity.
Use a boundary between the two
Most applications need both. Keep streams in data access and state coordination, then expose a signal to the template when that makes rendering simpler.
readonly customer = toSignal(
this.route.paramMap.pipe(
map((params) => params.get('customerId')),
filter((id): id is string => Boolean(id)),
switchMap((id) => this.customersApi.getById(id))
),
{ initialValue: null }
);
The observable handles route changes and request cancellation. The template receives a straightforward signal. This boundary prevents a component from becoming a mixture of subscriptions, manual cleanup, and unrelated UI logic.
A decision table
| Situation | Better default | | --- | --- | | Toggle, selected value, or derived UI state | Signal | | HTTP request based on changing input | RxJS | | WebSocket or live feed | RxJS | | Local computed display state | Signal | | Multi-step async workflow with retries | RxJS | | Template reading a stream result | Convert at the boundary |
Migrate in stages
Do not rewrite every observable because signals are available. Start with component-local state where the benefits are immediate. Leave established data-fetching, caching, and streaming workflows in RxJS until there is a concrete reason to change them.
For each migration, verify that you have not lost cancellation, error handling, loading behavior, or cleanup. A smaller API surface is valuable only if it preserves the behavior users rely on.
Common pitfalls
- Creating effects that perform hidden writes to many signals.
- Converting streams to signals too early, then losing access to useful operators.
- Replacing a well-tested observable store without a measurable problem to solve.
- Treating signals as a global state-management strategy by default.
Signals and RxJS are complementary tools. Use signals to make state close to the UI easy to read, and use RxJS to model the asynchronous reality of a production web application.