4 ms·
I’m not particularly comfortable with some of the nature of the change. Runes are magic compiler symbols, but they look even more like just normal code than was
by chrismorgan 3y ago
I’m not particularly comfortable with some of the nature of the change. Runes are magic compiler symbols, but they look even more like just normal code than was the case before. Previously, Svelte’s reactivity model was very easy to understand, including its limitations, because it was the result of very simple analysis—you can cover the rules in a few minutes without difficulty. It included some things that were obviously magic: $: reactive blocks and $ prefixes on stores.
When you had this:
let count = 0;
count += 1;
… it made reasonable sense, because that’s just normal JavaScript; the fact that `count` was made reactive was basically incidental.
But once it’s this:
let count = $state(0);
count += 1;
This looks like you’ve called a function named $state, and given that you’re talking about migrating from compile-time to runtime reactivity, you might (I think reasonably) expect `count` then to be some object, not just an integer, and so `+= 1` would be the wrong (since JavaScript doesn’t let you overload those operators). But no, it’s instead some kind of decorator/annotation.
Yes, stores are unwieldy as you scale them up, and definitely have some practical problems, but that createCounter stuff looks fragile. I’m curious how it all works, because it looks like it’d be needing to do quite a lot of control flow analysis, but not curious enough to investigate at this time. But my intuitions suggest it’s probably markedly more complex and difficult to explain, though perhaps and hopefully more consistent.
- rich_harris 3y ago> it looks like it’d be needing to do quite a lot of control flow analysis Implementation-wise, this is vastly simpler than Svelte 4, because everything is explicit. One thing we didn't really show today is how this works in your editor in practice — for example, TypeScript thinks `$state` and `$derived` are just the identity function. It makes sense when you use it, but I appreciate that I'm basically asking you to just trust me on that! > Previously, Svelte’s reactivity model was very easy to understand, including its limitations, because it was the result of very simple analysis I totally understand what you're saying. But in fact the analysis is _anything but_ simple — in places it's frighteningly complicated, to the point that even I sometimes don't understand what's going on! And that's part of the problem — because Svelte 4 is more implicit, you can't really know what's happening in a lot of cases, you just have to hope that the compiler knows what it's doing.
- chrismorgan 3y agoFair enough. I’ll see how things go and wish you well. For simple cases, the new will be syntactically-inferior, but it makes sense that the explicitness allows it to be more consistent. Ah, modelling mutable trees as stores… not a great deal of fun.
- eyelidlessness 3y ago> for example, TypeScript thinks `$state` and `$derived` are just the identity function That seems like a missed opportunity on Svelte’s part… but hard to fault, because TypeScript doesn’t support nominal primitive types very well. Ideally it would be something like Reactive<T> to signal (ha) that it’s not just a plain value.
- rich_harris 3y agoWe've toyed with this idea. There's a couple of problems though. Firstly, if you have this... type Reactive<T> = T; function $state<T>(value: T): Reactive<T> ...then TypeScript will 'unwrap' the type anyway, unless you do funky stuff like this... type Reactive<T> = T & { [uniquesymbol]: any }; ...in which case things like `value += 1` will cause type errors because it coercies `value` from `Reactive<number>` to `number`. But it also creates problems here: let message = $state('hello'); obj = { message }; The type of `obj.message` is Reactive<string>, but it's _not_ reactive — it's a non-reactive snapshot of the value when the object was created. It's possible that we can do some fun stuff with TypeScript plugins, but we haven't dived too deeply into it yet.
- eyelidlessness 3y ago> TypeScript will 'unwrap' the type anyway, unless you do funky stuff like this […] in which case things like `value += 1` will cause type errors because it coercies `value` from `Reactive<number>` to `number`. Yeah. I’ve spent more time than I’d like to admit trying to find a nice solution to this, and I’ve ultimately arrived at “it needs type system support to work for the general case”. I think you can make your brand (symbol) optional to address this case, but it doesn’t address this: > The type of `obj.message` is Reactive<string>, but it's _not_ reactive — it's a non-reactive snapshot of the value when the object was created. And here is the real problem: the types are right (in this case)! It’s Svelte that is wrong (in that it deviates from the JavaScript language semantics, which TypeScript has correctly modeled). This leads to my other ultimate conclusion with nominal primitive types: most of the time you’re better off just boxing the value… unless, or until, it becomes problematic for other reasons (eg performance, which would probably be a concern here, albeit one worth testing). Boxing allows correct, refined typing; it makes semantics of the value clear. The only downside is convenience… but this sort of convenience is exactly the kind of thing people rightly judge about the JS ecosystem. I’m not likely to use Svelte (for a variety of other reasons), so I don’t have a particular dog in this race… but I will say I find more explicit types for signals a great deal nicer to work with. Whether the explicit mechanism is a function call (like Solid) or an accessor (like many others).
- jameshart 3y agoFunctions can return primitives, so there’s nothing wrong with $state() returning a number that you can +=1. What is weird is that it’s actually typed as a svelte-compiler-only ‘extended int’ that can have side effects when you increment it, and weird knock on results from reading it.