Skip to content
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

Different reactivity triggering of props (automatic derived inserted sometimes) #15406

Open
thes01 opened this issue Feb 28, 2025 · 13 comments
Open

Comments

@thes01
Copy link

thes01 commented Feb 28, 2025

Describe the bug

I wonder why sometimes a prop getter is wrapped in $derived, such as:
<Component name={state.name.toString()}/>

which results in the following compiled output:

const expression = $.derived(() => $.get(state).name.toString());

Component(node, {
	get name() {
		return $.get(expression);
	}
});

and other times it is used directly in the getter:
<Component name={state.name}/>

which results in:

Component(node, {
	get name() {
		return $.get(state).name;
	}
});

This behavior seems to be present in Svelte since v5.0. The specific issue I'm having is that this has consequences for reactivity triggering inside the child components. The minimal reproduction is in the REPL.

I assume there is some reason to do this. However, this feels inconsistent (magical) to me. In our codebase, we sometimes want to rely on the "not derived" behavior, i.e. update things even when the content is the same.

An example use case where this actually matters would be having an input field that triggers some on_change when a user inputs something. But in some logic above the resulting value is computed/overwritten to be the same as had been before (thus it should overwrite back what user has typed in). By wrapping the prop in $derived, the signal doesn't update the prop value, so the user's input stays in the input field.

Reproduction

https://svelte.dev/playground/0e8e303022c24b609f4c13b58ef2df9d?version=5.20.5

Remove .toString() on line 7 in App.svelte and now the compiled output doesn't use $derived.

Logs

System Info

reproducible in REPL

Severity

annoyance

@paoloricciuti
Copy link
Member

We do this when there's a function call involved...this is because you don't want the function to be called everytime (which would happen if we didn't wrap it in a derived)...however the bug feels the fact that it's rerunning even if name didn't actually changed. I suppose is because what is changing it's actually the whole object which triggers the effect. Mmm i don't know how i feel about this.

@thes01
Copy link
Author

thes01 commented Feb 28, 2025

I see, but in this case, I'd actually want the function to run each time.. If I didn't, I'd use an explicit derived or const.

Otherwise, I'd expect that a reassignment of $state.raw or $state with new reference(s) retriggers all props related to it.

In other words, I like the mental concept of "props are just getters, derived/const triggers differently".

@paoloricciuti
Copy link
Member

The point is that the props you are passing is just the value and with fine grained reactivity it the value doesn't change it shouldn't rerun...that's why I'm surprised it does and I wonder if we should actually fix this in some way 🤔

@thes01
Copy link
Author

thes01 commented Feb 28, 2025

Hmm.. So you mean that all props should be automatically derived?

Makes sense, although it complicates the above use case a little bit. Sometimes you simply need to rerun things even when the value didn't change.. (but it's still doable if you pass the whole object as a prop)

On the other hand, the state of the art can be surprising, because it behaves differently for:

<Component
    prop={some.nested.thing}
>

and

{@const thing = some.nested.thing}
<Component
    prop={thing}
>

(we have already stumbled upon an issue with this in our codebase but then we thought that's a feature not a bug)

@thes01
Copy link
Author

thes01 commented Feb 28, 2025

If I'm right though, you'd still get the "nonderived" behaviour when doing an effect:

$effect(() => {
   some.nested.foo;
});

I guess that should/would trigger no matter if foo actually changed its value or not..

@paoloricciuti
Copy link
Member

Makes sense, although it complicates the above use case a little bit. Sometimes you simply need to rerun things even when the value didn't change.

Can I ask you what's the use case here?

@thes01
Copy link
Author

thes01 commented Feb 28, 2025

An example use case where this actually matters would be having an input field that triggers some on_change when a user inputs something. But in some logic above the resulting value is computed/overwritten to be the same as had been before (thus it should overwrite back what user has typed in). By wrapping the prop in $derived, the signal doesn't update the prop value, so the user's input stays in the input field.

I guess it's generally when working with inputs. The reason is that input has it's inner state and sometimes you need to overwrite what the user typed in. But when the overwritten value (the prop) hasn't changed itself, the overwriting doesn't happen.

I can prepare a REPL to illustrate it better :)

@paoloricciuti
Copy link
Member

An example use case where this actually matters would be having an input field that triggers some on_change when a user inputs something. But in some logic above the resulting value is computed/overwritten to be the same as had been before (thus it should overwrite back what user has typed in). By wrapping the prop in $derived, the signal doesn't update the prop value, so the user's input stays in the input field.

I guess it's generally when working with inputs. The reason is that input has it's inner state and sometimes you need to overwrite what the user typed in. But when the overwritten value (the prop) hasn't changed itself, the overwriting doesn't happen.

I can prepare a REPL to illustrate it better :)

If you can that would be great...but i would say don't do this in effects

@thes01
Copy link
Author

thes01 commented Feb 28, 2025

So here it is: https://svelte.dev/playground/420aa35591fe41aa91eb20e81d24973d?version=5.20.5

This one is a bit different than what I tried to describe. And I must say I'm a bit confused now why it does not work as I expected :D.

The case here is that you have an input that automatically appends a unit ("mm") if the user puts a unitless number in it. So if you put "100" then it updates the state and the input to "100 mm".

The issue is when you have "200 mm" in the input field and then you just delete the units and then press enter. The value itself ("200 mm") didn't change, but now the state is different from what the user sees in the input (there is still only "200" in the input field).

@paoloricciuti
Copy link
Member

I think this is a different issue but there's not much svelte can do here...you can change your strategy and do something like this by setting the value within flushSync and then resetting it later. But i would probably change the whole strategy of how you handle this.

@thes01
Copy link
Author

thes01 commented Feb 28, 2025

Ok, I assume there might be another issue there. Thanks for the updated example, although it feels more like a hotfix than an actual solution. By changing the strategy you mean like don't even automatically change the value of the input? Or some other way to accomplish the same?

Getting back to the previously discussed...

The point is that the props you are passing is just the value and with fine grained reactivity it the value doesn't change it shouldn't rerun

Well I don't know what fine-grained reactivity really means exactly.. I feel like it is more connected to the deep/proxied $state. By using $state.raw you explicitly opt-out from this and use $deriveds when you want to have a cached value.

@paoloricciuti
Copy link
Member

Btw the problem specifically with inputs is that we check if the value is the same and don't update the input (if i'm not mistaken)

@thes01
Copy link
Author

thes01 commented Feb 28, 2025

Yea might be, I quickly skimmed through the source code and there is a check element.value === value included. But still I would suppose it to work since element.value should be in sync with what is typed in..?

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

2 participants