svelte
svelte copied to clipboard
Svelte 5: A mutable `$derived` rune that allows easy passing and changing of reactive functions and dependencies
Describe the problem
I'm digging the new reactivity system of runes, but there's a sticking point to me, and others I've come across. There's a clunky awkwardness when passing formulas meant for $derived
into objects. I used a class as an example here, but I reckon this will be encountered in a lot more contexts like passing props down to components, use directives, functions that return reactive objects or values, etc.
This Calculation class is meant to be told what kind of calculation to do in its construction. See the awkwardness here:
let a = $state(1)
let b = $state(2)
class Calculation {
#formula = $state()
output = $derived(this.#formula())
constructor(formula) {
this.#formula = formula
}
}
const adder = new Calculation(() => a + b)
// adder.output would be 3 to start
const multiplier = new Calculation(() => a * b)
// multiplier.output would be 2 to start
There is another way to accomplish this without using $derived
, by using the $state
able formula function directly. Even though it accomplishes the same thing, and is slightly cleaner, it's still kind of clunky because it looks like you are calling a function, when really you want to be thinking of it in terms of a derived value. This makes me think about the original reason for sprinkling Svelte magic on top of signals, to get away from "calling" signals in order to get their values.
class Calculation {
formula = $state()
constructor(formula) {
this.formula = formula
}
}
const adder = new Calculation(() => a + b)
// adder.formula() is 3
Describe the proposed solution
A reactive $derived
can be implemented simply by leveraging the fact that $derived
is not assignable, and making it assignable in such a way that it swaps its reactive function upon a new assignment. But since we may not want to break intuitions and the importance of immutability, we can keep the fundamental immutability preserved in the base$derived
and introduce an alternative kind of $derived
, like a $derived.mutable(...)
.
Here is an example of a mutable $derived:
class Calculation {
output = $derived.mutable(/* some default reactive function */)
constructor(formula) {
this.output = formula
}
}
const adder = new Calculation(() => a + b)
This would make life much easier, cleaner, and I think lines up nicely with the Svelte ethos of "Why not simplify this?" while not taking the power and flexibility away.
In short,
- let stateFn = $state(someFn)
- const value = $derived(stateFn())
+ let value = $derived.mutable(someFn)
I think this would be really handy, particularly for classes. What do you all think?
Importance
would make my life easier
An alternative approach other than to have $derived.mutable(...)
might be to use an external helper function to modify a derived function.
Currently:
const value = $derived(() => a + b)
value = () => a - b // Error! Can't assign to a derived! Rightfully so? Maybe...
Then maybe we can have a function like swap
that takes a derive and switches the function to another one.
import { swap } from "svelte"
const value = $derived(() => a + b)
swap(value, () => a - b)
And in full for class benefits:
class Calculation {
output = $derived()
constructor(formula) {
swap(this.output, formula) // or even formula.bind(this) to access class properties from the function
}
}
const adder = new Calculation(() => a + b)
A third approach might be to allow the use the specific case of empty $derived()
or $derived.once
to act as a placeholder for a function waiting to be assigned. So that it can be set once but then becomes immutable after its first initialisation. This might be more difficult to program the compiler for though. It would likely have to resort to throw runtime errors. But maybe we can hijack the same mechanism that TS uses to see if things are "definitely assigned in the constructor". Just some thoughts.
I personally don't like this solution but I thought I'd throw it out anyway.
class Calculation {
output = $derived.once // or maybe $derived()
constructor(formula) {
this.output = formula
}
}
const adder = new Calculation(() => a + b)
adder.output = () => a -b // Error! This derived was already defined.
The solution feels worse than the problem, if I'm honest — this doesn't feel like a large enough issue to warrant new runes, since we want to keep the API surface area to a minimum. Both rune versions are closer to $derived.by
than $derived
, but it's hard to see that, so it feels a bit confusing. The swap
idea wouldn't work, as imports don't (and shouldn't) have special meaning to the compiler.
Thanks for your response, Rich...
I definitely didn't intend to introduce a completely new rune. I meant a rune variation. Perhaps the title was misleading. At most a flag on the existing $derived rune...
I get how swap() would break the pattern of having special meaning to the compiler. That makes total sense. But why don't you like the first $derived.mutable
option? That it would compile similar to $.set(derivee, newFn)
upon assignment. I'm guessing it's because the "get" type (evaluated expression/function) is doesn't align with the "set" type (the function itself), which is fair enough. But wonder if there are other qualms you have?
Personally, I will say I do think you're underestimating the problem. Maybe the solutions I proposed are not the best but I definitely think this needs to addressed in some way. At the end of the day, as the developer, I would like to have the option to change my $derived
functions in general. 15 hours ago or so ago another poster commented the exact same issue. I think the people are encountering this and it's blaring how un-Svelte-like it is.
Having a double-decker $derived of a $state to accomplish this feels like an unnecessary overkill in principle (maybe you haven't ridden the London buses in a while and that makes you feel like it's not a problem 😂 - kidding I love those). It feels limiting to me to not have that option to modify that as the developer if I choose to.
I believe as people use runes more and more, this issue will become more pronounced, as it's something that's generally encountered when there's higher complexity. It would be good to nip it in the bud while we're young!
Personally, I will say I do think you're underestimating the problem.
More a lack of convenience than a problem IMO, maybe even a safeguard against the "mutable derived" pattern which can end up being confusing at some point. Since the use case here is a reusable helper there is no real downside to be explicit over concise.
Adding a rune or a sub-rune would endorse this pattern which seems bad (and weird since it would be a contraption helper unlike all others).
by using the $stateable formula function directly. Even though it accomplishes the same thing, and is slightly cleaner, it's still kind of clunky because it looks like you are calling a function, when really you want to be thinking of it in terms of a derived value.
This example is pretty good and indeed way more readable than the solution, it looking like it calls a function is actually clearer in my eyes since that's what you fundamentally want it to do. But at this point I can't really see if a helper is necessary, as in what would a legit real life use case look like?
Is the real problem that when using classes you have to write this?
class Calculation {
#formula = $state()
output = $derived(this.#formula())
constructor(formula) {
this.#formula = formula
}
}
...and it would be solved if you were instead allowed to write this?
class Calculation {
constructor(formula) {
this.output = $derived(formula());
}
}
One could make the case that we should loosen the restrictions a bit here and allow $state
and $derived
assignments to class fields if they're at the top level of a constructor.
That is what I suggested here:
- #11116
This example is pretty good and indeed way more readable than the solution, it looking like it calls a function is actually clearer in my eyes since that's what you fundamentally want it to do. But at this point I can't really see if a helper is necessary, as in what would a legit real life use case look like?
By this logic we would never even have a $derived rune in the first place, since it's always "calling a function". But the point of $derived is that you think of it as a value. As the Svelte motto goes about frameworks being organised around the way you think.
I mean I was under the impression the whole point of runes was to encapsulate these kinds of patterns that would otherwise be verbose in ways that are simple and meaningful. If we want to be more in touch with what's going on under the hood, that's SolidJS... To me the idea of Svelte is about exposing what's meaningful and representative of our natural thought processes, while tucking the wires neatly under the hood.
If I have a $derived rune, I want to think of as a reactive value. Yes, it comes from an expression or a function, sure. But it's meaning to me as the developer is to think of it as a value and use it as a value. Because I'm not actually thinking about calling the functions. Functions, to me, are more side-effecty do-ey things, where values are more get-y is-y things. $deriveds fit more into the ladder camp IMO. To me the $derived's expressions are more functional in that their only purpose is to return a value, but its only the value we care about, so the wires are tucked away.
Now... it would unlock a lot of power and flexibility to simply be able to change the expression a particular $derived variable is using under the hood. Right? Why not? I think this is the case where there is understandable amount of resistance to change but actually if you sleep on it you'll realise everybody wins from this.
As for the use cases... There are many use cases. But I could ask you what's the use case of $state
when I can just do createSignal()
and value
and setValue
? $state
is easier to think about, to keep your code organised, to present simplicity out of complex code. I'm telling you now from my week of intensive writing of fairly complex rune classes with all sorts of inheritance and encapsulation and nested stuff, this issue I'm addressing here is a sticking point for me. It's one of the few things that feel clunky about runes. It's hard to explain why... call it dev instinct ?
By this logic we would never even have a $derived rune in the first place
No I meant your specific example of a formula executor taking a formula
argument is kind of a counter-example since executing a function might be what the consumer expect here.
If anything I would be in favor of allowing assignments in constructors but I understand Rich's conservative point of view as the more inconvenient this gets the more niche the use case certainly is (or the pattern not adapted), genuinely curious about the actual use case here.
If anything I would be in favor of allowing assignments in constructors but I understand Rich's conservative point of view as the more inconvenient this gets the more niche the use case certainly is (or the pattern not adapted), genuinely curious about the actual use case here.
I'm not a fan of the constructor assignment version because I like to know mentally that the structure of my class is defined outside of the constructor. For me personally, if I am creating a $derived
property in a class, to me that's a structural thing, and so it should be specified (though perhaps not defined) outside the constructor.
class DailyMeals {
// declaring the structure outside constructor (👍)
snacksEaten = $state(...) // Ah yes, someState is a reactive variable (👍)
totalCalories = $derived(...) // Oh, yes, I see totalCalories is a reactive derived value (👍) and I can see its an immutable derived (👍)
constructor( totalCaloriesFn ) {
// ... defining values, functions, etc, NOT structure (👍)
// but I can't define or change the totalCalories derived function to totalCaloriesFn (🙁)
}
}
I want to define the "shape" and reactivity of my properties outside of the constructor, and then deal with the values within the constructor.
Here is the proposition that @brunnerh suggested which, although I agree with the problem, I don't see it as the best solution. Here's why:
class DailyMeals {
// outside constructor, declaring the structure (👍), but there is a misinterpretation (🙁)
snacksEaten = $state(...) // Ah, yes, I see, someState is a reactive variable (👍)
totalCalories // Total calories is what exactly? Looks like just any regular property? (🙁) I might mistake this for a non-reactive property because I can't see any kind of rune on it.
constructor( someFn ) {
// code code code
this.totalCalories = $derived(someFn) // <--- Oh look, totalCalories is actually a reactive variable, but I had to look deep in the constructor to find that out (🙁)
// code code code
}
}
That's why I am banking on the mutable derived value version. A mutable derived is defined outside of the structure, so at a glance I can gauge the behaviour of the properties of the class. I can see their structural properties like their type, their reactivity, etc. If there is a $derived() assignment nested somewhere in the constructor, I will lose that at-a-glance feel where I can reliably assess my class's structure.
class DailyMeals {
// declaring the structure outside the constructor (👍)
snacksEaten = $state(...) // Oh, yes someState is a reactive variable (👍)
totalCalories = $derived.mut(...) // Ah yes! This is mutable derived value (👍) where might it be defined?
constructor( someFn ) {
// ... defining values, functions, etc (👍)
// lots of code
$derived.set(this.totalCalories, someFn) // <--- oh look, I found where the mutable derived is defined (👍)
// some more code
}
}
No I meant your specific example of a formula executor taking a formula argument is kind of a counter-example since executing a function might be what the consumer expect here.
You're inadvertently addressing my point exactly. The way I'm forced to write it makes it seem like this is a function to be called but in reality I only care for the value. All $deriveds are functions under the hood as you know. But we write derived because we only care for the value, and we only want to think of the value.
Let me draw a parallel to explain this point. Here's a simple reactive addition class:
class Add {
a = $state(0)
b = $state(0)
sum = $derived(this.a+this.b)
}
I am deliberately choosing sum
to be a derived property because I'm only concerned with its value. If I wanted it to be a function, I would have done getSum() { return this.a + this.b }
. But I want it to be treated as a reactive value. So that whenever a
or b
changes, whatever is depending on sum is reacted with as if it were a value. This is the entire point of why this is useful. It gives the developer the flexibility to decide when something should be thought of as a value or when it's best to think of it as a function to be called. That's what makes $derived so great in the first place, and this is a natural extension of its functionality.
Having a mutable $derived allows for the above kind of class to be flexible in its calculation, while still, from the owner of the instance of a function, treating it, thinking of it, using it, as a value - not as a function. I don't want to think of my script "running" sum(). Rather, it's retrieving a value called sum
, and that value might be updated in the future. If you think of it, this is entire point to $deriveds. Otherwise we would just store functions in $states and call them whenever we need them. Practically speaking that would accomplish everything deriveds, except you would have ()
at the end of all your "values". You can literally do away with $deriveds completely with that mentality.
So, you propose to introduce a new rune only to get rid of one line and one variable? It is not a common case, and I struggle to see the benefits, so it isn't worth it for me.
I can't rewrite the title unfortunately. If you read my description, I recommended a sub-rune, but I'm not married to that idea. I like the idea that $deriveds
being immutable by default, in the sense you can't change the expression they were created with. I also want to have the ability to have a mutable $derived
for special use cases where it's handy to swap the expression. In the same you have "sub-runes" like $state.frozen
, which slightly modify the behaviour of a rune.
It doesn't matter, rune or subrune - it is one more thing to learn for users and one more thing to maintain for the dev team.
Talking about the mental understanding of the code, I don't get your point. If a class/object allows to customize a method of computation of something, it must store the current method of the computation; it's a part of the class/object's state.
Plus, often, $derived is overused. The valid use cases are memorizing "heavy" computations used in multiple places and preventing over-reactivity by memorizing. In the rest of the cases, it's doubtful. Example.
Plus, often, $derived is overused. The valid use cases are memorizing "heavy" computations used in multiple places and preventing over-reactivity by memorizing. In the rest of the cases, it's doubtful.
That's a good point you make. From a certain perspective, the only purpose of $derived is to essentially cache the value, because, as you touched on, the "triggering" of reactive deps are handled by signals nested in the function anyway. In many instances it can be replaced by a regular function. But in the ladder situation the function will be called again everywhere it is accessed whenever any of the deps change.
When you do encounter a situation where you want to be swapping expressions (imagine that Calculation class but you can change the calculation on-the-fly), and "caching" or "memorizing" the result, this is exactly where a mutable $derived would help simplify things and bring in the elegance that Svelte is all about. It's also precisely why it's more meaningful to think of $deriveds
as values rather than functions being called. Because the function isn't being called everywhere it's being accessed. It's only called once per update.
The way I see it right now, the fact that we can't change the expression that a $derived is using is actually taking away. It's almost like disabling something that I feel should be built-in. I feel as the developer I should be making the decision whether I want a $derived that can switch expressions. I don't like the idea that the framework is blocking me from doing this.
At the end of the day, I'm just a dev sharing my perspective. Take it or leave it. There are still many good things Svelte & runes has to offer. I promise if this was a feature people would find hundreds of good uses cases for it, probably writing cleaner code and increasing performance. Right now, you have to workarounds similar to what you made in the REPL, getters, cached values, encapsulation, etc. It's awkward and clunky, and it really doesn't need to be.
I can't be the only person thinking of derived
as being quite cumbersome? Why not something easier on the eyes, tongue and ears... result
perhaps? Isn't that what it is?
@Rich-Harris I'm also really struggling with some usecases in Svelte 5 due to the limitations around mutability in derived values.
What I effectively want is a sort of rune where it's updated whenever a dependency changes, but is also mutable.