-
Notifications
You must be signed in to change notification settings - Fork 59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The end-goal should be to make framework compilers unnessecary #198
Comments
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, 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% |
🎉
I can adapt existing code that doesn't yet use signals, but you can play with this 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 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, I hope this helps! and if there are any questions specific to utils that can be made with the proposal, there is a |
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 with
Computed
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
The text was updated successfully, but these errors were encountered: