svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Svelte bindings is invalidating properties that don't change causing infinite loops

Open mildred opened this issue 5 years ago • 2 comments

Describe the bug

When using a component with bind:property={property}, the binding function generated by this is invalidating more than necessary in the parent component, and this is causing an infinite loop.

I asked this on discord:

Hi, I got a simple problem, but I cannot see the end of it. I have an application with two components. The parent component includes the child component with a two way binding (bind:property={property}).

Elsewhere in the parent component, I have a line with:

$: property = defaults.property

So whenever the defaults change (which is not very often, at the start of the app or when the settings are reloaded) the property will be reset to the defaults value.

Now, I could debug that when the child component modifies the property, the generated binding function is called, and it invalidates the property (as expected) but it also invalidates the default variable (which is not expected) triggering an infinite loop.

Removing the $: property = defaults.property code fixes the infinite loop, but then my app does not work as expected.

Does anyone have ideas on how to solve infinite loops like these? I'm on it for hours and cannot find the solution.

I then found a workaround:

Just taking a calm moment, I found a solution to my problem above:

I replaced:

<script>
  ...
  $: property = defaults.property
  ...
</script>
<Child bind:property={property} />

with:

<script>
  ...
  let property = defaults.property

  $: defaultsChanged(defaults)

  function defaultsChanged(defaults){
    property = defaults.property
  }
  ...
</script>
<Child bind:property={property} />

And now, when the child component updates the property, defaults is no longer invalidated and no infinite loop. That's very surprising though.

To Reproduce

I don't have a small reproducible example, but I have a full repository: https://github.com/mildred/calcul-meubles/blob/svelte-bug-20200515/src/ensembles/Ensemble.svelte#L29-L43

Expected behavior

Svelte should not invalidate properties in this way

Information about your Svelte project:

  • Your browser and the version: Firefox 75.0
  • Your operating system: Fedora 31
  • Svelte version: 3.22.2
  • Whether your project uses Webpack or Rollup: Rollup

Severity How severe an issue is this bug to you? Is this annoying, blocking some users, blocking an upgrade or blocking your usage of Svelte entirely?

This is quite problematic since it takes forever to debug, and to use the workaround, you need to find the correct variables that invalidates incorrectly. But once you know what to look for, it's easier to debug, and there is a workaround.

Additional context

Method to debug:

  • don't get your computer in a bad state, prepare to kill your browser just in case
  • log property changes extensively, try to find the property causing the loop
  • refresh with developer tools closed, wait until firefox ask you to stop the script, then open the devtools
  • just look at $$invalidate\(.*, variable_name\) in the generated source, set breakpoints for the offending variables
  • notice that there are tow invalidate where there should be only one.

mildred avatar May 15 '20 12:05 mildred

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[bot] avatar Dec 24 '21 19:12 stale[bot]

I think this is the same issue that we have run into. Minimal example:

<script>
	const WX = {};
	export let manifestId;
	let foo;
	$: view = WX.View({
		manifestId,
	}, state => {
		foo = 'x';
	});
</script>
<input bind:value={foo} />

If you put this into the REPL, notice that the generated code for the binding looks like this:

	function input_input_handler() {
		foo = this.value;
		($$invalidate(0, foo), $$invalidate(1, manifestId));
	}

This is wrong - why is the binding invalidating manifestId? It should just be invalidating foo.

Commenting out either the manifestId option to WX.View or the foo = 'x' line results in the expected code which is this:

	function input_input_handler() {
		foo = this.value;
		$$invalidate(0, foo);
	}

In practice this is also causing infinite loops for us.

SystemParadox avatar Jul 08 '22 17:07 SystemParadox

Probably the same issue here: https://stackoverflow.com/q/75655792/546730

Example being given:

<script lang="ts">
  export let data: PageData;
  $: roleName = data.role.name;
</script>
<TextInput bind:value={roleName} />

Where the input value cannot be edited because the binding invalidates data, causing an immediate reset via the reactive statement.

brunnerh avatar Mar 06 '23 23:03 brunnerh

@Conduitry I think there should be enough repros now.

brunnerh avatar Mar 06 '23 23:03 brunnerh

This comes down to $: standing for two things and it's not easy to distinguish:

  • side effects that happen in reaction to something
  • derived state that keeps values in sync

Svelte 5 fixes this by separating these into two distinct runes, $effect and $derived. That way it's much clearer what's going on and such edge cases are avoided entirely. In this case, you would have a $state variable and either update that through the binding of from an $effect, resulting in the desired behavior.

dummdidumm avatar Nov 15 '23 11:11 dummdidumm