Angular Signals: The New Mental Model When Signals shipped, a lot of teams heard "the RxJS replacement" and started converting everything — every , every async pipe, every stream in the codebase — into a signal. That instinct is understandable and it's wrong. Signals don't replace RxJS. They solve a different problem, and the two are meant to coexist, each doing the job the other was never built for. Two different problems, not one upgraded solution Signals model synchronous, fine-grained state. A signal holds one current value, notifies exactly the parts of the template that read it when that value changes, and is always readable synchronously — there's no "subscribe and wait," just . derives a new signal from others, recalculating only when an actual dependency changes. runs a side effect whenever its dependencies change. This is the right tool for: is the sidebar open, which tab is selected, what's the current filter text, is the form valid. No subscription, no async pipe, no teardown to think about. Just a value, read directly. RxJS models asynchronous streams over time. A search box's input, a WebSocket's messages, an HTTP call that might fail and need retrying, a rate-limited sequence of events — these aren't single values, they're sequences of values arriving on their own schedule, and they need the vocabulary the previous two lessons covered: cancellation, rate-limiting, combination, recovery, sharing. Signals have no equivalent to or , because they were never built to model a stream — only a current value . Where the confusion actually costs you The expensive mistake isn't using Signals — it's using them for the wrong half of this split. This compiles, runs, and looks modern — and it silently reintroduces the exact stale-response race condition the first lesson in this chapter used to eliminate. has no cancellation model. Each keystroke's search now runs to completion independently, and a slow response to an old search term can still overwrite a newer, correct result. Converting an async stream into a signal-driven effect doesn't remove the five problems operators solve — it just removes your ability to solve them cleanly. Where Signals genuinely replace RxJS Plenty of usage in existing Angular codebases was never modeling a stream at all — it was synchronous UI state wearing RxJS's clothes because that was the only reactive primitive available at the time: Same behavior. No subscription, no async pipe, no risk of forgetting to unwrap it in a new template. This conversion is a pure win, because the state genuinely never involved asynchronous timing in the first place. The principle that transfers React drew the identical line, just with different names. and are React's signal and computed — synchronous, fine-grained, local reactivity, read directly with no subscription. Asynchronous streams are still a separate concern in React too, usually handled by plus , or better, a dedicated library like TanStack Query that already encodes cancellation, rate-limiting, and sharing. The split isn't an Angular-specific quirk of Signals versus RxJS — it's the same synchronous-state-versus-asynchronous-stream distinction every framework eventually has to draw, whatever it calls the two halves. The Trade-off: Which Problem Are You Actually Solving? Signals removed real boilerplate for synchronous state. That's not in question. The trade-off worth examining is what happens when the same instinct gets pointed at genuinely asynchronous work. The pain, concretely v1: A component has three pieces of purely synchronous UI state — a panel's open flag, the active tab, a filter string — each modeled as its own with its own async pipe in the template. Works, but it's nine lines of subscription ceremony for values that never involve timing at all. v2: More simple state accumulates the same way as the component grows — every new boolean or string gets its own subject. The component becomes a small subscription farm for values that are, in reality, just variables. v3: Someone "modernizes" the component by converting everything to signals — including the one genuinely asynchronous piece, a live search, using a hand-rolled that calls directly, as shown above. v4: The stale-response race condition from Lesson 1 returns, because the hand-rolled effect has no cancellation logic and signals have no operator model to borrow one from. The bug looks new. It's the same bug, wearing newer syntax. Side-by-side Signals for everything Signals for sync state, RxJS for async streams --- --- --- Simple UI state (toggle, tab, filter text) Clean — this is Signals' actual strength Clean — same result, same tool A live search with rapid input Hand-rolled cancellation inside , or none at all + , tested and battle-worn Recognizing which tool a piece of state needs Everything looks the same; the async cases are easy to miss Explicit: "is this a value, or a stream?" answers it every time Boilerplate for trivial synchronous state None, if using signals…