Recently, we've seen many developers trying to replace RxJS by creating their own "operators" for Angular's signals. This approach is understandable, but it can lead to subtle errors. To avoid them, it's essential to understand the distinction between two reactivity models: Push and Pull.
Push (Observables): Every Pushed Value Matters
In the Push model, the emitter (the source) is in control. It emits values into an "observable" stream, and each emitted value exists independently. These values are then processed, one by one, by those who are observing them.
Imagine a production line: each part that passes exists independently. If you stand along the line to observe, you'll see every part pass, without exception. If you arrive late, you've simply missed the first few parts. They've been sent, and they aren't waiting for you.
๐ This model is ideal for managing data streams, where every value is important.
Pull (Signals): Only the Last Pulled Value Matters
In the Pull model, the consumer is in control. A value is only of interest when it's explicitly read. The consumer will "pull" the value when they need it. This implies that transient values that are not read are irrelevant.
Imagine a digital display board at a train station. It's constantly updated, showing real-time information about trains. There might be dozens of updates per second, but the traveler only sees the last version displayed when they look up. The intermediate updates don't matter to them. Only the latest version of the board's state is relevant.
๐ Signals are therefore perfect for managing a state where only the latest version matters.
Why Some Operators Make Sense and Others Don't
This distinction between Push and Pull is crucial for understanding why certain operators work with signals and others don't.
โ What Works: Operating on the Final State
Operators that don't care about the history and only react to the change in the final state will work well with signals. For example, a double
operator is perfect for a signal. It simply reads the current value, multiplies it by two, and returns the result. It needs nothing else.
const count = signal(2);
const doubledCount = computed(() => count() * 2);
console.log(doubledCount()); // 4
โ What Doesn't Work: Needing the History
Operators like filter
or take
are designed to work with data streams. Adapting them to signals is risky. Why?
-
The
filter
trap:Imagine a signal initialized to
1
(const counter = signal(1)
) and a resulting signal (acomputed
for example) that only keeps even numbers. If you docounter.set(2)
, thencounter.set(3)
, the resulting signal will never see the value2
. In fact, if you only pull the final value, the signal will only consider the last value, which is3
(odd), and filter it out. Thus, the result will never be what you expected.
const counter = signal(1); const evenCounter = computed(() => { const value = counter(); return isEven(value) ? value : undefined; }); counter.set(2); counter.set(3); console.log(evenCounter()); // undefined => instead of 2
Conversely, if the last value is
4
, you'll get the false impression that your operator is working correctly.
const counter = signal(1); const evenCounter = computed(() => { const value = counter(); return isEven(value) ? value : undefined; }); counter.set(2); counter.set(3); counter.set(4); console.log(evenCounter()); // 4 => tricky result
-
The
take
trap:Similarly, with the same
counter
signal initialized to1
, if you performcounter.set(2)
, thencounter.set(3)
, thencounter.set(4)
, atake(3)
operator will only consider the last value (4
). It will have no idea that the values1
,2
, and3
ever existed. For it, this is its first (and only) iteration.
In Summary
Signals are optimized for managing the application's state (Pull model), while RxJS is perfect for managing data streams (Push model).
Before using one or the other, ask yourself the following question:
"Do I need to know the history of the data, or does only the latest value matter to me?"
If only the latest value matters, signals are for you. Otherwise, an observable is probably the better solution.
Top comments (0)