Skip to content

Reactive declaration on a store, caused by a change in another store from another component is not called #5365

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
lovetoast opened this issue Sep 7, 2020 · 22 comments
Milestone

Comments

@lovetoast
Copy link

Describe the bug

ComponentA setting the value of a store (call it store1), which calls a function in ComponentB as it is dependent on this store. The function in componentB sets the value of another store (call it store2) and has a reactive declaration for store2 (just a console log, or anything else for that matter), the reactive declaration is NOT called.

This appears to be because the initial trigger is from ComponentA. If I update store2 from ComponentB, it calls the reactive declaration on store2.

To Reproduce

Please click here:

https://svelte.dev/repl/f51251e28e1a42d292de15b1a6079476?version=3.24.1

Top.svelte sets setCategory (type the value in the text field and hit SetCat and it updates it). Annoying.svelte listens for this and calls on onSetCat, which console logs (to make sure it's running) and then sets setSearch, which should show an alert, but it does not (only shows on page load, when the value is set from stores.js. There is a button in Annoying.svelte, called try locally, and this DOES trigger the reactive declaration. I've included commented code for a traditional subscribe function, to show it does not work with this either.

Information about your Svelte project:

  • Version 85.0.4183.83 (Official Build) (64-bit)

  • Windows 10

  • Reproduced on repl which I believe is the latest?

Severity

Umm going to be honest, I like this bug, because it actually makes my live search function work a bit better, but hey, I'm an honest guy.

@lovetoast
Copy link
Author

@bfanger
Copy link
Contributor

bfanger commented Oct 3, 2020

A more visual REPL demonstrating the issue

bfanger added a commit to bfanger/svelte that referenced this issue Oct 7, 2020
@bfanger
Copy link
Contributor

bfanger commented Oct 7, 2020

The ordering inside the compiled $$self.$$.update = () => { function matters, a minimal example:

<script>
  let amount = 2
  let price = 1.99;
  let total = 0

  $: formattedTotal = total.toFixed(2)
  $: calculate(amount) // $: calculate doesn't work when placed below the $: formattedTotal

  function calculate(n) {
    total = price * n
    console.log('calculated '+ total)
  }
 </script>

{price} x <input type="number" bind:value={amount} /> = {formattedTotal}

REPL

This makes the reactivity system unreliable, i'll see if I can fix the issue or provide a meaningful warning message.
It's quite tricky as I don't want to allow circular dependencies.

@lovetoast
Copy link
Author

The ordering inside the compiled $$self.$$.update = () => { function matters, a minimal example:

<script>
  let amount = 2
  let price = 1.99;
  let total = 0

  $: formattedTotal = total.toFixed(2)
  $: calculate(amount) // $: calculate doesn't work when placed below the $: formattedTotal

  function calculate(n) {
    total = price * n
    console.log('calculated '+ total)
  }
 </script>

{price} x <input type="number" bind:value={amount} /> = {formattedTotal}

REPL

This makes the reactivity system unreliable, i'll see if I can fix the issue or provide a meaningful warning message.
It's quite tricky as I don't want to allow circular dependencies.

Hi Monsieur Bob, thanks for your reply, but, I may have been mistaken, is this not a bug at all and instead how svelte handles the micro-processes?

PS I was just reading a stack-overflow answer on Svelte, from last month regarding #key and I was thinking this sounds weird but awesome, I'm hoping it was written by an authority, and hey, it was you!

@bfanger
Copy link
Contributor

bfanger commented Oct 8, 2020

Definitely a bug. when you write:
$: formattedTotal = total.toFixed(2)
it shouldn't matter if it's at the start or at the bottom of the script tag, that should just work.

I do expect resistance to a solution. That will need some iterations to get right.

@guidobouman
Copy link

guidobouman commented Jan 12, 2021

Another repro in the REPL:
https://svelte.dev/repl/8180d687add34e76b57d5b98508154b0?version=3.31.2

As you can see, the moment a function is defined reactive with a parameter, it does not trigger any subsequent reactive updates. While executing the function the moment the initial reactive variable is changed does trickle down all subsequent changes.

@guidobouman
Copy link

@bfanger Maybe change the title of this issue to reflect the actual bug?

@bfanger
Copy link
Contributor

bfanger commented Jan 13, 2021

@guidobouman I did not report the issue, I did figure out why it happens and created an RFC which outlines a possible solution.

@dummdidumm
Copy link
Member

Both the REPL by @guidobouman and the issue outlined in the RFC by @bfanger are actually expected behavior given how reactive statements currently work. The key is that the code is analyzed beforehand and only the values immediately part of the declaration are taken into account for ordering. This is explained in more detail in this issue and this section of the community docs. The latter also explains why this behavior actually is desirable for some people because you might want to hide specific values from reactivity tracking.

@guidobouman
Copy link

@dummdidumm
Okay, we can shield a variable from reactive tracking with a function. But what if, in a function, you edit a variable that is used by another reactive variable? Then the reactivity breaks when the order is not correct. The compiled code from my example showcases this: $$invalidate() is compiled into the function.

This does not seem like intended behavior. Especially when it starts working the moment you change the order of the reactive declarations.

@dummdidumm
Copy link
Member

This at the moment is the intended behavior, because as described in the issue linked above, you hide the variable from the ordering algorithm because it's inside the function. You need to make it a direct dependency of the $-statement like $: activeVideoKey = switchToIdle(targetState);. The reactive algorithm does not look into functions referenced in $ to determine the order.

@guidobouman
Copy link

guidobouman commented Jan 13, 2021

For my understanding: Then where does the $$invalidate around the reactive var inside the function come from? 🤔

If the compiler adds the $$invalidate method, then I would expect this part of the linked comment to also become true:

For example, if one block updates foo and another uses foo, the block that updates foo will end up before the other one in the compiled code, even if it were second in the component definition.

@dummdidumm
Copy link
Member

The $$invalidate and the ordering of the reactive statements are not related to each other. The sentence in the linked issue explains it: "In your component, the compiler cannot tell that the second reactive block might cause changes that should cause the first block to be re-run, because the updates to activated (in your case activeVideoKey) are hidden in the activate() (in your case switchToIdle) function. So it defaults to keeping them in the order you wrote them"

@guidobouman
Copy link

guidobouman commented Jan 18, 2021

Okay, $$invalidate and the reactive statements sorting are different and unrelated. But can the dependencies not be tracked through the same mechanism that adds the $$invalidate calls? Maybe to influence the sorting or otherwise to improve the reactivity.

I hoped that the $$invalidate call, that is wrapped around all reactive statements, could be used to make this work.


PS: The RFC explains the issue nicely:

Svelte reactivity system is invisible, even considered "magic", when it doesn't work as expected, it's very hard to debug.

In the current implementation, reactive statements are not always run or are not run with the latest values, this depends on the ordering inside the generated code [...]

@dummdidumm
Copy link
Member

In issue #5905 the user wants to have the opposite, no reordering. If we would change the behavior as desired in this issue right here (also tracking stuff inside functions), the user in #5905 would have no possibility to write code in a way that does not get reordered.

@guidobouman
Copy link

Uhmm... It gets reordered anyhow, right? This would not change that.

Actually, I kinda agree with the request from #5905. But combined with deeper dependency tracking from this issue.

The one thing I want for in every platform / framework / codebase is predictability. How easy is it to explain it to a newcomer without resorting to caveats. Magic sorting is quite a caveat. Especially when it goes against the line by line execution known to nearly all programming languages, including JavaScript. If you build invisible reactivity, it needs to work in every case.

It seems reasonable to want to shield code from reactivity but still react on some variables. A function could still provide that. Could this maybe be achieved if you only track variables in functions that are referenced in the actual reactive statements?

@madupuis90
Copy link

I also just ran in this "bug"... you can check an oversimplified REPL of my code (I use a custom store in real code) :

https://svelte.dev/repl/cb8853b5fe804b9a84417704ba85f6db?version=3.38.3

I feel this is pretty unpredictable and create bugs that are hard to catch if the component is semi-complicated.

In this case, I don't want to shield code from reactivity, I want to react when the value of a store changes, regardless of the order.

@dummdidumm
Copy link
Member

It works as expected if you replace bar.set(!$foo); with $bar = !$foo . Question is if that is consistent or not. On one hand it's consistent because the rule of reactive statements is "every variable that is directly referenced is used to define the order, on the other hand it's confusing because the statements are semantically the same.

@madupuis90
Copy link

I used .set in my example for simplicity sake but the real code has a custom store and I don't expose .set. I only expose some state changing functions. So in this case $bar = !$foo would not work

@stale
Copy link

stale bot commented Dec 23, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale-bot label Dec 23, 2021
@guidobouman
Copy link

I think this issue is still relevant.

@dummdidumm
Copy link
Member

This will be working as expected in Svelte 5. Dependencies are tracked at runtime now, and with the $effect and $derived runes everything will rerun until it has settled.

@dummdidumm dummdidumm added this to the 5.x milestone Nov 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants