Modern Angular
Signals Without the RxJS War
Signals are excellent for current state and synchronous derivation, and shipped stable in Angular v20 (May 2025). RxJS is still the better language for debounce, cancellation, polling, retry, and events over time. The boundary is the design decision; the rest is consequence.
The wrong question
The least useful version of this conversation is asking whether Signals replace RxJS. That question makes teams rewrite Observable contracts or force async workflows into Signals just to look current.
The better question is: what kind of problem is this state solving? If it is a current value or a synchronous derivation, Signals usually fit better. If it is a stream over time with debounce, cancellation, retry, polling, or WebSocket semantics, RxJS is still the tool I want.
Alex Rickabaugh (Angular team) framed the line the same way on JS Party episode 310: 'Signals are values that can change over time, and RxJS observables really give you notifications of events happening at a specific point in time.' The framework's own team teaches Signals and RxJS as different primitives, not competing ones.
The split I use
| Situation | Default choice | Reason |
|---|---|---|
| Active tab, filter text, expanded row | Signal | Local UI state with one current value. |
| Derived label, permission flag, filtered count | computed | Synchronous derivation from other values. |
| Existing user$ or permissions$ service | toSignal at the component boundary | Keep the service contract and simplify template reads. |
| Autocomplete with debounce and cancellation | RxJS pipeline plus Signal for UI state | Time and cancellation belong in the stream. |
| NgRx or ComponentStore already coordinating a feature | Both | Keep the store backbone; expose Signals where they help consumption. |
The boundary I like
A useful team policy is simple enough to remember in code review: use Signals for local and derived component state; keep RxJS for streams, async composition, events over time, NgRx integration, and cancellation-heavy workflows.
Interop earns its place when it makes the boundary explicit via toSignal and toObservable. It is not a mandate to convert the whole application. A component can read a user Signal derived from user$ while the authentication service keeps exposing Observables because that is still the right contract for the rest of the app.
When the Angular team communicates adoption, it tends to communicate with production data. In the v20 announcement, YouTube reported a 35% improvement in interaction latency on Living Room after switching to Angular Signals via Wiz. That result lands when the team treats Signals as the right primitive for UI state, not when it tries to retrofit Observable state into Signal shape.
In the search example, normalize the query before distinctUntilChanged(). Otherwise angular and angular become different requests, and the stream is doing avoidable work.
export class HeaderComponent {
private readonly auth = inject(AuthService);
readonly user = toSignal(this.auth.user$, { initialValue: null });
readonly canAdmin = computed(() =>
this.user()?.roles.includes('admin') ?? false
);
}private readonly api = inject(SearchApi);
readonly query = signal('');
private readonly query$ = toObservable(this.query);
readonly results = toSignal(
this.query$.pipe(
map(query => query.trim()),
debounceTime(300),
distinctUntilChanged(),
switchMap(term => {
if (!term) return of([]);
return this.api.search(term).pipe(
catchError(() => of([]))
);
})
),
{ initialValue: [] }
);The trap
Pawel Kozlowski (Angular team, RFC Signals author) put the distinction concretely on JS Party 310: 'A signal — I can always look at this box, and I'm going to get the value. If I subscribe to an event emitter, there was no event fired, I don't have a value.' That distinction is what makes effect the wrong tool for keeping two Signals in sync.
effect is not the new subscribe for everything. The smell is an effect that exists only to keep another Signal in sync. Use it when reactive state needs to touch an external system: localStorage, analytics, a canvas, or an imperative library. If the rule is derivation, prefer computed. If the rule is time, prefer RxJS.
Reusable artifact
Signals/RxJS boundary rule
- Value now: Signal.
- Value derived now: computed.
- Events over time: RxJS.
- Legacy Observable consumed by a component: toSignal at the boundary.
- External side effect from reactive state: effect, sparingly.
Sources checked
- https://angular.dev/guide/signals
- https://angular.dev/api/core/signal
- https://angular.dev/api/core/computed
- https://angular.dev/api/core/effect
- https://angular.dev/api/core/rxjs-interop/toSignal
- https://angular.dev/api/core/rxjs-interop/toObservable
- https://angular.dev/api/core/rxjs-interop/rxResource
- https://rxjs.dev/api/index/class/BehaviorSubject
- https://github.com/angular/angular/discussions/49685
- https://github.com/angular/angular/discussions/60121
- https://blog.angular.dev/announcing-angular-v20-b5c9c06cf301
- https://changelog.com/jsparty/310
Modern Angular Playbook
This article is one play.
The Modern Angular Playbook collects the diagnostic, the adoption matrix, eleven plays, and the 30-day plan. Free, in English and Portuguese.
You get both PDFs by email, through the Dojo IA list.
André Ramosavailable for remote roles, UTC−3Get in touch →