Skip to content

Svelte 5: value in template becomes unreactive under seemingly random circumstances #11012

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

Closed
Evertt opened this issue Apr 1, 2024 · 6 comments
Milestone

Comments

@Evertt
Copy link

Evertt commented Apr 1, 2024

Describe the bug

Well, the best way I can describe the bug is by showing you. This will take at most 3 minutes of your time.

https://youtu.be/4JdoGHOIY4k

In short, since I know that Svelte uses a stringify() function in its render_effect() functions for its templates, which looks like this:

function stringify(value) {
    return typeof value === 'string' ? value : value == null ? '' : value + '';
}

I know that if I have anything that's not a string (e.g. an object), but that can be casted to a string (e.g. if it has a toString() method), then it can be used in Svelte 5 templates plainly and Svelte's stringify() function will make sure it's casted to a string. I'm experimenting with how this can enable me to create objects and / or functions that "just work" in templates. So far it always worked well, but this time it stops working, but only under very specific circumstances. Watch the video to see what I mean.

Reproduction

Here is the REPL, so you can play around with it.

But I really recommend you watch the video to see when things go wrong in the template and stop being reactive.

Logs

No errors, just the template not updating under specific (but seemingly random) circumstances.

System Info

Using the REPL which uses Svelte v5.0.0-next.90.

My browser is Arc:
Version 1.36.0 (48035)
Chromium Engine Version 123.0.6312.87

Severity

annoyance

Edit

I now see that depending on what line I uncomment / active, the generated output code changes dramatically.

When my increment function looks like this:

const increment = () => {
    count.value = count + 1
}

Then the generated output code looks like this:

const increment = () => {
    $.mutate(count, $.get(count).value = $.get(count) + 1);
};

// And then a little later
$.render_effect(() => $.set_text(text, `count is ${$.stringify($.get(count)())}`));

$.render_effect(() => {
    $.set_text(text_1, `count is ${$.stringify($.get(count).value)}`);
    $.set_text(text_2, `count is ${$.stringify($.get(count))}`);
});

But when my increment function looks like this:

const increment = () => {
    count(count + 1)
}

Then the generated output code looks like this:

const increment = () => {
    count(count + 1);
};

// And a little later
$.render_effect(() => $.set_text(text, `count is ${$.stringify(count())}`));
text_2.nodeValue = `count is ${$.stringify(count)}`;
$.render_effect(() => $.set_text(text_1, `count is ${$.stringify(count.value)}`));

Notice how suddenly Svelte doesn't bother generating $.mutate() or $.get() code in the increment function anymore? Even though that shouldn't even be necessary anyway... And notice how text_2 has been taken out of the render_effect()? That's clearly why it stopped being reactive. But why Svelte would make these choices is beyond me...

Edit 2

The only thing I can guess, is that for some reason Svelte concludes that when I use count(count + 1) (or count(count() + 1) I've also tried), that the plain count cannot by reactive. Probably because it thinks that functions cannot be reactive. But since functions can in fact be adapted to have a [Symbol.toPrimitive]() method attached to them and therefor can be casted to a string which could reactively call some kind of inner state, I would recommend that Svelte stops assuming functions cannot be reactive.

Because also, the cost of being wrong in that scenario, is negligible. If you indeed create a render_effect() like so:

$.render_effect(() => $.set_text(text_2, `count is ${$.stringify(count)}`));

And it turns out that count is indeed a plain function that produces no reactive output when it's casted to a string, then you've lost nothing, because then that render_effect() will never run a second time...

@7nik
Copy link
Contributor

7nik commented Apr 1, 2024

Markup reactivity triggered by implicitly called methods isn't and won't be properly supported: see #9860.
{foo} (bare variable without accessing any property or methods) considered as static unless the variable is defined using a rune. However, it still may be rendered (see #10629 (comment)).

Regarding your REPL, it seems count is considered reactive when it is mutated but not used in any rune.

P.S.: to maintainers: I'm kinda surprised that <p>count is {count()}</p> always lives in its own render_effect and isn't optimised by merging render_effects.

@Evertt
Copy link
Author

Evertt commented Apr 1, 2024

@7nik Okay, I see this ticket is a duplicate of #9860, the one that you created. However, I don't understand the reasoning of Conduitry in his response to your ticket.

If supporting this requires any additional runtime code for people not using a custom toString(), I don't think we should support it.

Why? I don't understand the problem. Seriously why? Look, the fact of the matter is that because Svelte chooses to intentionally make it impossible to pass a $state("with a primitive value") around as a raw signal, it means that we (the community) can't build new primitives or runes that build on top of Svelte's primitives / runes. So we can't recreate VueUse or Solid Primitives, which are both super awesome and useful projects.

Or of course, we can recreate those primitives, but then we have to deviate in our API design from Svelte's API design. Suddenly, we need to create ref()s that do require you to access their inner value using .value for example. Which I think is awful, especially once you've been spoiled with the beautiful lack of .value when using Svelte's native reactive primitives.

So in any case, I was hoping that at least, I could create a ref() that would have the following benefits:

  1. It would allow the consumer of the ref to choose whether to use it as a function or with .value, depending on what looks cleanest to them.
  2. It would still allow the consumer to use the count that comes out of const count = ref(1) as a plain value whenever possible. For example when doing count(count * 3) (notice how I didn't need to call count to get its primitive value), or when printing it in the template using {count}.

These small things can just help to reduce any visual noise when reading code and therefor are important to people like me.

Again, it would be even better if Svelte would just allow the community to create our own primitives that have the exact same ergonomics as Svelte's native runes, but I'm skeptical that that's ever gonna happen.

@7nik
Copy link
Contributor

7nik commented Apr 1, 2024

the reasoning of Conduitry in his response to your ticket

My understanding: to support this feature, almost any {foo} should be treated as potentially reactive and be rendered using render_effect instead of one-time text.nodeValue = $.stringify(foo), and this leads to additional overhead (no idea how big this is) that isn't worth it. Maybe the overhead isn't critical during runtime but is during hydration.

You can destructure $derived to make reactive primitives: example (ignore temp - cannot inline rn due to a bug).

@Evertt
Copy link
Author

Evertt commented Apr 1, 2024

and this leads to additional overhead (no idea how big this is) that isn't worth it.

Yeah, well, I disagree. Because the additional overhead would be a one-time overhead on the very first render. After that, if the underlying variable is indeed non-reactive, then it won't ever re-render anyway and so then there's no additional overhead.

You can destructure $derived to make reactive primitives: example (ignore temp - cannot inline rn due to a bug).

Ah damn, if it wasn't for that temp bug then I think that might solve all my problems... Are you sure that's considered a bug by the team and will be fixed?

edit

Never mind, I saw the issue you just opened about this and it does indeed look like a bug.

@7nik
Copy link
Contributor

7nik commented Apr 1, 2024

As I said, the overhead may be critical during the hydration — when a user sees a rendered page but isn't reactive yet. When this phase becomes long enough for the user to try to interact with the page, but nothing happens, it leads to a negative and confusing experience.

Maybe the team measured the overhead, maybe not, IDK.

@Rich-Harris Rich-Harris added this to the 5.0 milestone Apr 1, 2024
@dummdidumm
Copy link
Member

We shouldn't assume that everything is reactive due to perf reasons, therefore closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants