I have a root component with a piece of data.
This root component includes several different intermediate components.
Each of these intermediate components uses the same leaf component.
All of these components use two-way binding to share the same piece of data, from the root, all the way down to all the leaf components.
When the leaf component changes the data that is shared with two-way binding, an infinite loop occurs.
Reproduction is at https://github.com/TehShrike/svelte-repro/tree/two-way-binding-infinite-loop
To reproduce,
npm run buildindex.html fileThe issue only happens if both IntermediateComponent and EvenMoreIntermediate are included in TestComponent. Including multiple instances of just IntermediateComponent or just EvenMoreIntermediate does not cause the issue.
I can reproduce this, but I'm not actually sure whether it's related to just the nested object setup. I removed the on:mouseleave handler, leaving just the on:mouseenter handler, and everything looks like it's working fine. There's probably something differently Svelte could be doing here, but perhaps it's more related to something like attempting to handle an event while currently handling another event.
For debugging this (with both event handlers in place), it doesn't help that my browser freezes even with breakpoints inside both event handlers! I didn't even know that could happen.
Actually, it looks like what's causing the problem is specifically the null in on:mouseleave="set({ currentIdentifier: null })". If I use another value in there instead of null, it seems to work fine. Any value I've tried, even other falsy values, even undefined. It seems to somehow be specifically triggered by null, which is quite mysterious to me.
As far as I can tell, this is related to typeof null being 'object', which is one of those fun javascript nuances I'd forgotten about. Changing dispatchObservers to continue the loop in that case seems to resolve this, but I'm not sure what else is going on or whether that's the best way to address this.
It must have something to do with the component structure - removing this line fixes the issue, too.
Okay, yeah, looks like my solution doesn't help if the value being assigned is actually a normal object, there's still the same infinite loop. Checking for null was just putting a bandaid on this one specific case.
@TehShrike
It must have _something_ to do with the component structure - removing this line fixes the issue, too.
But in this case, you do not get the structure you originally planned, right?
❓
I was just pointing that, when you remove that line, you are not rendering the exact/same structure and "sequence" of components.
Although it fixes the issue, in a real scenario, if someone needs to render an specific sequence of components similarly to what you had done initially, removing a component would not be a solution.
I only meant to point out that the infinite loop issue appears to depend on this specific component structure.
Ah okay, I understood.
I pushed a simpler/more specific reproduction to https://github.com/TehShrike/svelte-repro/tree/two-way-binding-infinite-loop .
Root contains multiple As, A contains multiple Bs, and B contains a list of Cs.
When C sets the bound value to null, the infinite loop occurs.
Now, like @Conduitry noticed, using some other falsey value instead of null fixes the issue here - but in my actual app, using some other value like false does not fix the issue. So I'm guessing that the null is a red herring and the bug is actually something trickier.
I wrote a test that I thought would be a straight copy of the reproduction above (which I tested in Chrome, Firefox, and Safari): https://github.com/TehShrike/svelte/commit/f6f4c228b5f7bc491ee1b6c0e7377a6c7e4c0b81
But the tests passed! I wasn't able to reproduce the infinite loop in the existing test structure.
Man, that was a real Heisenbug. Think I've got it beat though — happily, the solution is straightforward and only adds bytes to component binding code: #407
Most helpful comment
Man, that was a real Heisenbug. Think I've got it beat though — happily, the solution is straightforward and only adds bytes to component binding code: #407