Notes
Here's a collection of important notes on Signals, to give you a better understanding of their advantages and limits.
Magic
Signals introduce some "magic" as their related functions have important side effects.
If not well understood, the Signals could be misused, which may lead to code with bugs hard to inspect and fix.
One common error consists of creating an unwanted "reactive loop": a signal A
that updates another one, and from a hidden loop and chain of other Signals,
ends by updating the signal A
itself. This causes an infinite loop of updates, usually crashing the application.
The framework is able to detect such loops or signal writes in reactive contexts: it will report them as error instead of having a potential application crash. However, it assumes that the developers are responsible to limit these cases and avoid signal's contexts having side effects.
Moreover, it is suggested to read carefully the documentation to prevent "dangerous" practices or patterns.
Comparison with observables
A Signal is the sync version of an Observable. It contains a value that can be read at any time, and, just like Observables, it is capable of notifying interested consumers when that value changes. However, the Signals have complex and hidden side effects to be able to capture their context, thus they are not pure by design.
A Signal is extremely similar to a MulticastReplayLastSource, but leverage its functionalities, using specific functions and methods designed especially for them (these functions do magic stuff to make signals appearing sync).
Glitch free
The Observables are not glitch-free by nature.
Let's expose this with an example:
const [$counter, counter$] = let$$(0);
const isEven$ = map$$(counter$, (value) => value % 2 === 0);
const message$ = map$$(
combineLatest([counter$, isEven$]),
([counter, isEven]) => `${counter} is ${isEven ? 'even' : 'odd'}`
);
message$(console.log);
$counter(1);
Running this example gives the output which shows the glitch:
0 is even
1 is even
1 is odd
In asynchronous code, glitches are not typically an issue because async operations naturally resolve at different times. However, in some case, these inconsistent intermediate results can have unwanted or dangerous consequences for the application (like expensive and unnecessary operations, or soft lock). This may be fixed in various ways, like using function$$, but this is another discussion.
Because Signals are sync, they must be and are glitch-free in this implementation.
Slightly less performant
Using Signals adds a little extra layer of complexity that slightly reduces performance against pure Observables. So if performances are absolutely critical in a specific application, we may consider using Observables only. However, in most situations, you won't notice any performances' degradation, as we've done an amazing job to optimize every aspect of the signals.
Complement not replacement
Finally, Signals are a complement to the Observables, not a replacement.
A Signal is similar to a classic variable with the capacity to subscribe to its changes. Nothing more.
In consequence:
- to get async data (like mouse events, api fetch, etc.), Observables are the optimal way to fetch, stream and pipe these data.
For exemple, using
fromFetch
,fromAsyncIterable
,fromEventTarget
, etc. - a Signal cannot be cancelled, because this is not a pull source (where the subscriber ask for data), but a push source (where an emitter send data). So, if the data set into a Signal come from an async source, this source will have to be cancelled manually.
In conclusion, Signals are extremely practical, but they're not the optimal tool or magic answer of every situation, sometimes Observables are the proper answer, especially when we have to play with stream of data.