Signals, effects, and the death of the async pipe
Signals, effects, and the death of the async pipe
I removed the last async pipe from a production app. Why signals quietly took over the parts of my templates observables used to own — and the few places I still reach for RxJS on purpose.

On this page
I removed the last async pipe from a production Angular app last month. Not as a stunt — it just stopped earning its place. This is the long version of why signals have quietly taken over the parts of my templates that observables used to own, and the few places I still reach for RxJS on purpose.
The async pipe was always a workaround#
The async pipe solved a real problem: subscribe in the template, unsubscribe when the view tears down, and trigger change detection on each emission. It was the cleanest answer Angular had for "render this stream" without leaking subscriptions.
But it carried baggage. Every obj$ | async is a separate subscription, so reading the same stream three times in a template meant three subscriptions unless you wrapped it in an *ngIf="x$ | async as x" dance. Nested streams turned templates into a thicket of as aliases. And the value was always "maybe null yet" until the first emission, which leaked ?. and *ngIf guards everywhere.
What signals change#
A signal is a value you read synchronously and a dependency the framework tracks automatically. There is no "not yet" state, no subscription, no teardown. You read it like a property and the template re-renders when it changes.
readonly query = signal("");
readonly results = computed(() => this.filter(this.all(), this.query()));
The computed re-runs only when all or query actually change, and only if something is reading it. No manual combineLatest, no distinctUntilChanged, no shareReplay to avoid recomputation. The dependency graph is implicit and exact.
Converting a stream-heavy component#
The pattern I follow: push RxJS to the edges — the places where data genuinely arrives over time — and convert to signals as early as possible with toSignal.
private readonly route = inject(ActivatedRoute);
readonly id = toSignal(this.route.paramMap.pipe(map((p) => p.get("id"))), { initialValue: null });
readonly post = computed(() => this.store.byId(this.id()));
The router still hands me an observable — that is its API, and streams are the right model for "the URL changes over time." But the moment that value enters my component logic, it becomes a signal, and everything downstream is synchronous, null-safe, and trivially testable.
Where I still use RxJS#
Signals are state. RxJS is events. The line is sharper than I expected:
- Debounced search —
debounceTime,switchMap, cancellation. Signals have no native debounce, and faking it witheffect+setTimeoutis worse than just using the stream. - Coordinating multiple async sources that race or merge —
combineLatest,forkJoin,merge. This is what RxJS was built for. - Anything with backpressure or cancellation semantics — an upload that should abort when the user navigates away.
For everything else — derived view state, form values, toggles, the result of a single fetch — a signal is shorter, has no teardown, and never renders a flash of null.
The testing dividend#
The part nobody mentions: signal-based components are dramatically easier to test. No fakeAsync, no tick(), no marble diagrams for the common case. You set an input, read a computed, assert. The test reads like the component.
component.query.set("ssr");
expect(component.results()).toHaveLength(2);
That is the whole test. No subscription, no async, no flush. When the mental model of the code and the mental model of the test converge, you write more tests, and the ones you write are the ones that catch real regressions.
Closing#
I am not anti-RxJS. I am anti-ceremony. The async pipe made me pay a subscription-and-null tax on every derived value, most of which were not streams at all — they were just state I had modeled as a stream because that was the only tool. Signals gave the state back its natural shape, and left RxJS to do the thing it is genuinely best at: events over time.