DEV Community

LcsGa
LcsGa

Posted on

Angular Signals Don't Replace Observables: Pull vs. Push

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
Enter fullscreen mode Exit fullscreen mode

โŒ 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 (a computed for example) that only keeps even numbers. If you do counter.set(2), then counter.set(3), the resulting signal will never see the value 2. In fact, if you only pull the final value, the signal will only consider the last value, which is 3 (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 to 1, if you perform counter.set(2), then counter.set(3), then counter.set(4), a take(3) operator will only consider the last value (4). It will have no idea that the values 1, 2, and 3 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)