Open cysabi opened 6 months ago
One idea that was left open as part of the decorator proposal was to allow for a future addition of variable decorators. Using such a general-purpose feature, one could then choose to employ it specifically for signals if desired.
let @signal counter = 0;
effect(() => console.log(counter)); // Prints 0
counter++; // Prints 1
I favor an approach like this because we would get a general purpose language capability through decorators that could be used for all sorts of things, including signals.
aaaaand! If folks are open to classes (which, they're really good as long as folks don't abuse inheritance!)
you can have this today:
class GameBoard {
@signal turns = 0;
@signal
get next() {
return this.turns + 1;
}
nextTurn = () => this.turns++;
}
let board = new GameBoard();
board.turns // 0
board.nextTurn();
board.turns // 1
board.next // 2
using decorators, we don't need special property access, $
, or functions (State / Computed), -- with getters, we don't need to do anything special to know that we have reactive values -- it's business as usual, and any effect implementation is going to automatically do the right thing.
With decorators and classes, we achieve the goal of not using compilers while still having native JS "just properties" ergonomics .
aaaaand! If folks are open to classes (which, they're really good as long as folks don't abuse inheritance!)
you can have this today:
class GameBoard { @signal turns = 0; @signal get next() { return this.turns + 1; } nextTurn = () => this.turns++; } let board = new GameBoard(); board.turns // 0 board.nextTurn(); board.turns // 1 board.next // 2
using decorators, we don't need special property access,
$
, or functions (State / Computed), -- with getters, we don't need to do anything special to know that we have reactive values -- it's business as usual, and any effect implementation is going to automatically do the right thing.With decorators and classes, we achieve the goal of not using compilers while still having native JS "just properties" ergonomics .
I'm curious about this!! if what you're saying is true, I would push for this 100% could you share a slightly more complex example? how would this work with computeds?
I'm curious about this!! if what you're saying is true, I would push for this 100%
:tada:
could you share a slightly more complex example? how would this work with computeds?
I can adapt existing code that doesn't yet use signals, but you can play with this @signal
decorator here in signal-utils: https://github.com/proposal-signals/signal-utils
Here are the tests for the decorator: https://github.com/proposal-signals/signal-utils/blob/main/tests/%40signal.test.ts
Here is an example adapted from a "Toggle Group" component, where a reactive .value
will re-set the set of values that make up the active state for the toggle group (toggles where multiple can be active at once):
class MultiToggleGroup extends Component {
/**
* Normalizes @value to a Set
* and makes sure that even if the input Set is reactive,
* we don't mistakenly dirty it.
*/
@signal
get activePressed() {
let value = this.args.value;
if (!value) {
return new SignalSet();
}
if (Array.isArray(value)) {
return new SignalSet(value);
}
if (value instanceof Set) {
return new SignalSet(value);
}
return new SignalSet([value]);
}
)
here, @signal
wraps the getter in a Signal.Computed
, and then is mostly for referential integrity of the returned SignalSet
.
I hope this helps! and if there are any questions specific to utils that can be made with the proposal, there is a #signal-utils
channel in the signals discord https://discord.gg/vCPCVnfMHN, or you could open an issue for discussion on the signal utils repo!
why compilers exists
The goal of every framework is to support reactivity while feeling as close to native js as possible.
It's really obvious to anyone who's used svelte that the closer reactive code perceptibly feels to native js, the better the developer experience is.
signals without a compiler
Let's design an api for a non-compiler web framework, the goal is to make users feel like they are writing native js without using a compiler.
For setting signals, the closest we can get to this syntax is a proxy with a value accessor. I'll be using
.$
as the accessor.But what about reading signals?
Without a compiler, it's impossible to make getting the state reactive in a way that feels native. Since it gets recomputed all the time, we also have to wrap that in a function.
So let's invent
Computed
, it's basically a variable with a value that's declaratively based on another variable, rather than being imperatively set to whatever the variable is at the time.But a computed function can only work for variables we have the luxury of creating ourselves, what happens when we need to set some value that we didn't create? Like the dom?
Without inventing a new function, we could technically make all derived states eagerly compute, and shove a side effect into the computed function.
And anything that cannot be garbage collected would have some extra boilerplate.
But we can't really detect whether a
Computed
includes side effects or not, which means that every derived has to be eagerly pulled, and we don't want that. We want to only recompute when the values are actually needed by something outside. Looks like it's time for a new primitive!So we'll invent
Effect
, It's literally just a computed signal that's eager. And instead of returning value, it's used to imperatively set some values inside the function. We use this when we can't declaratively define them withComputed
alone.Since we don't return anything, we can just use the return value for cleanup functions
Ok, we basically invented solid signals wrapped with a proxy. It doesn't seem too bad to write, what's the problem?
the problem
If you're trying to pass a simple variable around, you have to be extra careful to always pass
state
and notstate.$
, otherwise you'll unwrap the proxy and lose the reactivity. You can't just pass the value around freely.With solid, I find it quite the chore to ensure i'm not accidentally calling my getter one step too early, and i find it really frustrating to debug when I do.
The existence of
mergeProps
andsplitProps
make this problem even more self-evident. You also get horrendous stuff likeconst children = children(() => props.children)
This is a core limitation of Javascript itself that makes it hard to design reactive frameworks to feel native without rewriting the syntax of js itself.
At it's core, compilers are trying to solve this limitation, letting users write native-looking js that can do non-native things.
inventing a compiler
So let's write a compiler to handle this stuff for us! Combine the value and reference into one, and unwrap the value automatically whenever necessary. That way we don't need to worry about this anymore.
Oops, we basically just invented svelte runes.
why we should tackle compilers
I think it's clear to me that all modern frameworks are dancing around this same core idea.
We want to express our ui in a declarative & reactive way that feels as native as possible, preferrably as close to the dom as possible.
The problem is javascript isn't declarative or reactive, and there are immovable walls to the developer experience because of that. With solid that looks like everything being wrapped in functions. With react that looks like blunt-force rerunning and component rules. With svelte that looks like a building a compiler so reactivity finally feels native.
But what if instead of needing to build a compiler to let you write native-looking code, we just... made it native?
I think it would be a huge waste of the best opportunity we have not to tackle this, it would solve the core issues plaguing every web framework at it's root.
an example proposal
What if javascript had a top level proxy? That's it. That's the entire proposal.
Then framework authors can implement signals to look as follows.
Keep in mind, this is a terribly shallow example of a proposal. I'm not trying to push that this is the practical solution to this problem. This would get rid of the need for compilers and mergeProps, but it would also probably be impossible to optimize.
It's intentionally extreme because the point is really the problem that this is trying to solve, my goal is just to hopefully convince you that this problem is worth solving at all.
At the very very least, I would like to steer the focus of this proposal to be on how it can be extended in the future to solve the need for compilers.
Some extra reading