RxJS Observables vs Signals

RxJS Observables vs Signals

Published:

Signals recently gained popularity in frontend web development. They are implemented by modern web frameworks like Qwik and SolidJS. Also, Angular adopted them. But what exactly are they, and do they replace RxJS Observables altogether?

Cover image

When I started using Qwik, this was the first time that I used Signals in practice, instead of only reading about them. One question that popped up was “Do we still need RxJS with Signals?” In this Post, we will cover what a Signal is and why RxJS still has its place.

What are Signals?

A Signal in software development is a data structure that holds one value at a time. This value can be read and written at any time. The Signal stores listeners that are interested in its value changes when they read the value.

When the Signal’s value changes, it notifies its listeners with this new value.

This is a pseudo implementation of a Signal:

type Listener = () => void;

let listener: null | Listener;

export function createSignal<T>(value: T) {
  const liseners = new Set<Listener>();
  return {
    get value() {
      if (listener !== null) liseners.add(listener);
      return value;
    },
    set value(newValue) {
      value = newValue;
      liseners.forEach((fn) => fn());
    },
  };
}

export function createEffect(callback: Listener) {
  listener = callback;
  callback();
  listener = null;
}

Any callback that is run via the createEffect() function will be executed immediately. The callback is registered as a listener within all Signals, that are read during this initial execution.

This is a very basic implementation, and it’s far from complete. There are various implementations tailored to the needs of the web frameworks which are using Signals.

What can we do with Signals?

Reactive state management

Signals can be used for reactive state management. We can place chunks of component state within a Signal. We can do this via useSignal() (Qwik), createSignal() (SolidJS) or signal() (Angular).

The useTask$() (Qwik), createEffect() (SolidJS) and effect() (Angular) functions let us run side effects on Signal value changes.

We can also derive new Signals from others via a useCompouted$() (Qwik), createMemo() (SolidJS) or derived() (Angular).

Likewise, we can let a signal trigger asynchronous operations or data fetching with the useResource$() (Qwik) or createResource() (SolidJS) functions.

Fine-grained reactivity

Web frameworks, that implement Signals, often use them to enable fine-grained reactivity. This means that, if a Signal’s value changes, not a whole component is re-rendered, but a local rendering is executed only in those places where the Signal has been read before.

When Signal values change synchronously, their dependent re-renderings are scheduled for one single synchronous render cycle. This prevents flickering when updating multiple spots within the DOM.

Signals vs. RxJS Observables

Signals as well as Observables are reactive data structures: They “emit” values over time. There are fundamental differences, though.

Signals are designed for simple state management: They are not lazy, as Observables are, and don’t have powerful operators that orchestrate subscriptions, as RxJS does.

This makes Signals much easier to understand than Observables. And they should be preferred for simple tasks like state management.

As I mentioned in Should I learn RxJS? , we should not overuse RxJS. We should not use it to solve simple problems where more basic solutions do their job well, unless we have a good reason to do so.

With Signals, it may be easier to detect more cases where the application of RxJS would be overkill and only complicate things.

As developers, it is our responsibility to pick a well-suited tool for the problem at hand. And in most cases of reactive state management, Signals are better suited. For other, more complex problems, though, it can be more reasonable to use the full power of reactive programming, that RxJS provides us.