This is a continuation to Why Observables Suck and what can you...
I never managed to swallow Signals, Hooks, and related imperative-reactive constructs. Why? Simply put, they really suck and here's why.
Take a look at the typical examples and how they claim Signal are "simple" to use.
const [thing, setThing] = Signal('initial');
const template = rml`
<button onclick="${setThing}"> poke me </button>
`;
Great, that's really simple, just like calling addEventListener()
.
The reality is, this story was made popular by a marketer.
You know, one of those guys who makes you believe the sky is blue and climbing the Everest as simple as walking, which is true, 'cause all you have to do is literally just walk, right?
Back to the sea level.
Web UI is crazy: async events, validation, authentication, API calls everywhere, page reloads, visibility change. A bit like freezing cold, clouds, storms on the Everest, headaches and embolism: anything could happen so asynchronously you may not even realise when you've just frozen into an ice block yourself in your long journey.
BTW, the sky is not blue either.
Signals are not a reactive construct
In a normal world, like at sea level, if you call setSomething('foo')
, nothing would happen, right?
Right, it's because Signals are not a reactive pattern. You need a crazy amount of other stuff, including proxies and dependency trackers of all sorts to intercept actions like this in the right order and cause the side effects you may expect.
Meantime, you already have Observables, which are way more similar to the Promises you use every day than Signals are to climbing the Everest... especially at scale.
The killer feature of Observables and libraries like RxJS is to make reactivity work just as good as Promises. They've been supercharged with all the high-level operators you need to do UI stuff, manage state, synchronise async events, API calls, wait for this, take 3 of that, etc. Observables are really the tools you need to climb the sssinking Everest (that was a pun, bad and intended) safely.
Signals want to make you believe Observables are hard, with all those map
, reduce
, filter
, switchMap
operators and all you need is just a [state, setState]
to get your stuff done.
Remember the marketing guy. Every word they say is half truth. The other half is
So, if you want to climb the Everest before it sinks, get proper equipment and training. If it won't kill you, it will make you stronger. Trust me, I'm no marketer.
What exactly can Signals not do?
Ok, we're now at the big question about imperative vs functional programming paradigms.
A bit like in construction, where you have architecture, plumbing, electricals before painting and decorating, we have something similar in software and UI, as well.
Turns out programming languages are not that good at that the former part: architecture. There are no architectural primitives to say wait until a button is clicked 3 times before making another dropdown active, and when it is, retry API calls from its selection changes if they fail, but notify the user and ask if they want to retry, up to 5 times, then give up in 7 lines of code.
Programming languages can add, multiply, move values here and there, call functions, methods or remote APIs and wait, but stuff like the above? You really want to use 7 lines of RxJS operators, rather than reimplementing all that logic from scratch, let alone deal with bugs.
Signals don't have operators
Turns out, we figured the problem.
When your UI gets very complicated you try to simplify your UX first, but there are things that are genuinely more complicated, like colour pickers and you have to face it.
Have we mentioned rendering glitches? Signals can emit whenever they like. If you have derived state sourced from other signals, they could come to you in random or unexpected order. If they do, you may end up rendering wrong data.
Turns out if your state is intricated enough to have circular dependencies (and yes, it will easily happen), making sure you always have the right data in place becomes a bit of a challenging task.
Have you ever thought of climbing the Everest? Hurry up, your grandchildren might well do the Olympus, one day.
Observables can of course emit when they like, too, but with one difference: you have zip
, merge
, scan
, combineLatest
, switchMap
, and a few other operators that control exactly how events of various types have to be synchronised and combined together. With signals, whatever you're going to do, you're going to at best reinvent these.
For Signals you don't even have something like Promise.all()
.
Do Signals actually suck?
We realised that Observables are great because of their operators, which Signals don't have. Even Promises are so much more ahead, with Promise.all()
, Promise.race()
, Promise.allSettled()
, Promise.withResolvers()
, etc.
Actually, I realised just writing this article that every problem is an opportunity and we could now create something new: Operators for Signals, currently just being a Stackblitz experiment.
Feel free experimenting with it, and show your love or hate. I'm really interested to get some feedback. I personally think this is still not a good idea, but you might well show me wrong! :)
Signal Operators in Action
Before finishing, another little example of what we could do with operators for signals:
import { Signal } from 'signal-polyfill';
import { effect } from './effect.js';
import { map, zip } from 'signal-operators';
const counter1 = new Signal.State(0);
const counter2 = new Signal.State(0);
const double1 = map(counter1, x => 2*x);
const sum = new Signal.Computed(()=>counter1.get() + counter2.get());
const both = zip(counter1, counter2);
effect(() => {
const [even, parity] = noGlitch.get();
console.log(`Even: ${even}, Parity: `${parity}`)
});
setInterval(() => counter.set(counter.get() + 1), 1000); // Changes the counter every 1000ms.
With regards to the big question whether Signals suck or not, I'm not 100% sure. At first I'm tempted to say yes, but time will tell. Operators sound like they could make them better, but Promises and Observables are Monads, so it's proven they have an immense compositional power.
Signals are not monadic
Signals are not monads, so they have a big disadvantage in terms of composability, which means, in plain terms, that your code will necessarily be much longer and more prone to bugs, compared to using Observables and their operators.
So, what do you think? Did this article help either changing your mind or consolidating any of your thoughts regarding signals or observables?