-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Svelte 5: $derived
usage in class depending on constructor args is convoluted
#11116
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
Comments
There are many advantages to the new reactivity system but the difficulty in passing reactive functions in the way you mention is one of the qualms I have. It's pretty clunky and awkward. I doubt the solution you proposed would be feasible, though. From what I understand, the declaration of reactive variables outside of the constructor is critical. If the compiler were to look for I do think it would be important to put our minds together and focus on this. This is a big sticking point for me. I have a suggestion that I'm not claiming is the right way but might help in direction, but it would be to have a mutable $derived value, which would simplify a lot and I think would be fairly easy to integrate. I'll write it out in a new issue and then link it. |
Note that if class Doubler {
#fn;
result = $derived(2 * this.#fn());
constructor(fn) {
this.#fn = fn;
}
} ...works just fine. Does that change the equation here? Obviously this would be nicer... class Doubler {
constructor(fn) {
this.result = $derived(2 * fn());
}
} ...but we decided not to do that because it's not free — it's less 'visible' when you have a more complex class, and the syntactical constraints become a bit more ambiguous (you can have There's no canonically right answer here, just trade-offs. Since could add |
For the first option example you gave, although that's prettier, it does give typescript error Also, in some of the particular cases, because of class inheritance issues and declaration orders, I have to use a In the particular cases I'm referring to, my best solution is actually to do: class Calculation {
getResult = $state()
constructor(fn) {
this.getResult = fn;
}
} I can get what I need using |
Ah, the TypeScript thing is a nuisance. Wouldn't be the first time it's constrained our API design. I do think it's important for clarity that state/derived fields are declared consistently as state/derived fields, in the same way that private fields must be declared up front (or in the TypeScript case, all fields). It occurs to me that the closest counterpart to this, where state is initialised in the constructor... class Counter {
count = $state()
constructor(initial) {
this.count = initial;
}
} ...would be this: class Doubler {
result = $derived();
constructor(fn) {
this.result = 2 * fn();
}
} This is basically what #11128 is asking for, but without any new runes. We would have the same restrictions as #11455, namely that you must assign the expression directly inside class Doubler {
#result = $.source();
get result() {
return $.get(this.#result);
}
constructor(fn) {
this.#result.fn = () => 2 * fn();
}
} |
Are you saying we implement #11128 and as a side effect we solve this constructor problem, or are you saying we take the idea from that issue but only use for this specific constructor case? |
I'm saying we turn this... class Doubler {
result = $derived();
constructor(fn) {
this.result = 2 * fn();
}
} ...into this: class Doubler {
#result = $.source();
get result() {
return $.get(this.#result);
}
constructor(fn) {
this.#result.fn = () => 2 * fn();
}
} |
Yes, but are we also allowing this then? let foo = $derived(count * 2);
// ...
foo = count * 2; If we only allow one but not the other it's very inconsistent, and either way I feel like it's unobvious if reassigning a derived means temporarily setting the value to something else until it's recalculated again or changing the derived formula. |
No, we wouldn't allow that. It would only be permitted in this case. Yes it'd be inconsistent but the only way to not introduce any inconsistency at all is to close this issue as |
Would |
I think the simplest way to solve all these problems is to allow re-assignment of If a And then we no longer have to do any of this sort of stuff of allowing one-time assignments or ts-constructor stuff. |
We definitely don't want to do that. In the vast majority of cases, if someone assigns to a derived value it's a mistake, and if we start to allow expressions to be reassigned then people will become extremely confused when <script>
let celsius = $state(0);
let fahrenheit = $derived(celsius * 9 / 5 + 32);
function reset_celsius() {
celsius = 0
}
function reset_fahrenheit() {
// today this is (rightly) a compile error. under this proposal, it would
// change the expression to `0`, unlinking it from `celsius`
fahrenheit = 0
}
</script>
<input type="range" bind:value={celsius} />
<p>{celsius}</p>
<p>{fahrenheit}</p>
<button onclick={reset_celsius}>set celsius to 0</button>
<button onclick={reset_fahrenheit}>set fahrenheit to 0</button> I'm talking about allowing it in a very specific situation: when you're in the |
Motivation: Show others who come here a pattern for getting something working and a low priority muse on how it could work in the future. As things stand, this is how I've gotten $derived usable in class fields, leaning towards explicitness with nulls. export class DoublerState {
num = $state<number | null>(null);
dbl = $derived<number | null>(this.num !== null ? this.num * 2 : null);
constructor(num: number) {
this.num = num;
}
} The caveat to the above is that if the derived field depends on data outside the class, it has to be brought in and set as a field to be referred to. So far that hasn't been an issue for me. Musing: Improving ergonomics to me would mean removing the need to handle nulls/undefined because there are no null use cases from the outside of the class, so something like: export class DoublerState {
num = $state<number>();
dbl = $derived<number>(this.num * 2);
constructor(num: number) {
this.num = num;
}
} Sorry I'm not aware of your constraints regarding knowledge of constructors having run etc. For fun I tried this, which feels like a possible compromise, however the compiler errors on this currently. export class DoublerState2 {
constructor(
initialNum: number,
public num = $state<number>(initialNum),
public dbl = $derived<number>(this.num * 2)
) {}
} |
EDIT: Not a good idea, see the comment below, leaving it here as an example. One simple solution I found is to just define the class inside a function: function createSomeClass(param1, param2){
class StateClass {
state1 = $state(param1)
state2 = $derived(this.state1 * param2)
...etc
}
return new StateClass()
} Works nicely for me so far, but I'm wondering what are the drawbacks of this approach 🤔 |
(Which is also why you get a warning if you directly construct a class, which is pretty much what happens here if you just inline the class definition.) |
Describe the problem
When reactively passing data to a class it has to be wrapped in a function or object. The class internally might want to use
$derived
to access it, but since that is currently only possible in a property initializer, any value passed into the constructor has to be stored in a separate property first.E.g.
REPL
Describe the proposed solution
Possibly allow reactive assignments in constructors:
If the
operand
does not need its own property:(Don't know if this is feasible or really worth it, but wanted to make a note of this.)
Importance
nice to have
The text was updated successfully, but these errors were encountered: