svelte: Svelte 5: $effect is unusable (produce circular dependencies and endless updates)

Describe the bug

This is a serious issue with $effect - it produces a lot of problems. See working example Svelte 4 and broken Svelte 5 version. It is impossible for me to so solve it with v5.

In Checks.svelte you will find a commented out $effect code, that I expect to work. But it does not work.

Then i added another $effect code below - that works, but not fully. I must use untrack - but it makes no sense here. Otherwise you will get endless iterations. The main problem seems to be with value.includes().

With this half working code, you can see 3 issues in REPL.

Additionally: one line is still commented out - I can not get it working. //if (!value) { value = []; return } //STILL DON'T WORK!!!

It is REALLY hard to use $effect as it is (specially compared with $: in v4). It needs to be improved, otherwise it will frustrate many people.

Reproduction

Svelte 4: https://svelte.dev/repl/9c8fbbe0824e4f4598a6c32e85119483?version=4.2.8

Svelte 5: LINK

Logs

No response

System Info

Svelte 5 Next 25

Severity

blocking all usage of svelte

About this issue

  • Original URL
  • State: closed
  • Created 6 months ago
  • Reactions: 1
  • Comments: 42 (24 by maintainers)

Most upvoted comments

Working solution

Reposting what I said in Discord:

You are mutating state in an effect that reruns every time that state changes, of course it’ll provide infinite loops. Similar to how a recursive function needs a stopgap (I forgot the technical term), these types of effects need a check to determine if they should run again or not (reassign again, in this case).

I used the lightweight lib dequal for this, although you could implement an equality check yourself if you wanted to.

See my previous comment. There is nothing wrong with $effect, it’s just the wrong tool for this.

Simpler code.

This is, IMO, simpler than the Svelte 4 example, easier to reason about (as in Svelte 4, it’s not clear why the reactive block does not run infinitely), safer, while maintaining almost the same amount of LOC.

Comparison:

// Svelte 4
$: if (disabled || !value) value = []
$: value = items2.filter(
	item => !item.disabled && value.includes(item.value)
).map(
	item => item.value
)
// Svelte 5
const is_valid_key = (key) => !!filtered_items.find(i => i.value === key && !i.disabled)

$effect(() => {
  if (value?.length === 0) return
  if (disabled || !value) return value = []
  if (value.some(v => !is_valid_key(v))) {
	  value = value.filter(is_valid_key)
  }
})

If you exclude the start and end of the effect ($effect(() => { and })), it’s the same amount of LOC. While avoiding all the issues that reactive blocks present, that I mentioned in previous comments.

Of course these effects are wonky. They’re just to show that a setTimeout yields different results.

That is true, setTimeout is not a true alternative.

Maybe instead we could have another variant of $effect which follows the naming scheme as $effect.pre, $effect.active, $effect.root , that is similar to the behaviour of $:

In that case we won`t need the debate anymore.

That could be quite the API pollution IMO. It would be super confusing to have two separate effects functions that do almost the same thing. At least active and root serve a different, and pre works the same, just happens at a different point in time.

Still, maybe it could be valuable. Up to the maintainers 😃

I personally worry about people seeing these changes happening to appease enterprise programmers while forgetting about the hobbyist.

The problem is that the static analysis that the reactivity system relied on previously was catching everyone off-guard, beginner and seasoned programmers alike. Reactive blocks would not run at times, or the order would that they are stated would drastically alter the output. There are several issues others have raised in GH that get fixed with runes (e.g. #6732).

Sure, compared to something that tried and “magically” do things for you, you’re now forced to come up with more responsibility, and it may feel odd. But it’s not out of the blue, things weren’t working out as intended, reactivity was quite leaky at times.

Reading though this, I must agree with OP. While nothing is logically wrong with $effect, I’ve come across this myself when converting Svelte 4 to Svelte 5 and it added a lot of complexity to the application.

My concerns isn’t so much about the right or wrongs of $effect but when comparing it to Svelte 4’s $: it adds complexity that may catch people out. As an experienced programmer, that is fine, but considering some of the reasons for people choosing Svelte it concerns me.

Svelte has always brought in programmers at all different experiences because “it just works”, simple logic like “when a value changes do this” and it working is what has brought joy to programmers of all experiences. I worry that adding this complexity would be a net negative to Svelte for DX.

I personally worry about people seeing these changes happening to appease enterprise programmers while forgetting about the hobbyist.

After looking into the issue, I think I did something similar to the obj before in my previous codes in React. The component was probably running in the loop. However, I never noticed that it updated itself.

If an error occurs (like in Svelte 5 or SolidJS), then I find that better than executing a loop unnoticed.

Even better if the compiler recognizes it and warns you.

Later… we need a collection of pitfalls in the docs, So nobody will run into such issues. Specially after Svelte 4 to Svelte 5 migration. $effect ist simple NOT a replacement for $: or afterUpdate etc… It’s different.

In svelte 4, the same result can be achieved with a setTimeout which in my opinion is much less of a hassle than having to write additional checks to avoid loops.

For the simple use case I demonstrated, that may work. But this is about predictability, which is important to ensure your code is reliable.

The $effect rune is predictable: if an internal state changes, it will re-run, that’s it. A reactive block with $: however, is unpredictable. If an internal state changes, it may re-run.

Also, what if it gets more complicated, with multiple setTimeouts?

EDIT: Of course these effects are wonky. They’re just to show that a setTimeout yields different results.

I already loathe the time where I will upgrade all my apps to runes and be greeted by infinite loops.

These are rare codes that do this.

There are currently 8 effects in my library. 3 effects (identical code) would create such a loop and with this help I was able to solve it.

My other effect code - I naively assumed it was analogous to Svelte 4 - also triggered endless loops for me. There I had simply reset an object to default. Like this:

...
let obj = {}

$effect(()=>{
//lets reset obj first...
obj = {w:0, h:0, x:0, y:0}

//now set on conditions...
if (...) obj.w=...
if (...) obj.h=...
if (...) obj.x=...
if (...) obj.y=...
})
...

@dm-de One important point you’re missing though. The loop “prevention” can be actively harmful.

Svelte 5 - Loop 👼 Svelte 4- No loop 👿

I want to share more information, about behavior in some other frameworks for comparison.

Summary: Effect produce endless loop in: Svelte 5, React, SolidJS Effect produce NO endless loop in: Svelte 4, Vue

This is an identical, but “useless” example to test endless loops.

Svelte 4: https://svelte.dev/repl/4d7c44fb38fd4baaa15f103c32b3ac7e?version=4.2.8

Svelte 5: LINK

React: run on https://playcode.io/react

import React, { useState, useEffect } from 'react';

export function App(props) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    //setCount(count+1) //endless loop :-(
    if (count>10) setCount(0)
  });

  return (<button onClick={()=>setCount(count+1)}>{count}</button>);
}

Vue: LINK

Solid-JS: https://playground.solidjs.com/anonymous/d06ed5ca-e0b0-4f40-be18-153dae755800

Nonetheless, I’ll try and make it simpler. But just because something has more LOC, doesn’t mean its worse, especially when we get the whole context behind these decisions.

@dm-de We don’t think $effect needs to be “improved”, more that we just need to do a better job documenting how it should be used and what to watch out for. It’s actually a very simple bit of logic – it re-runs when something inside it changes (sync). This is how it works in all other signal based libraries too.

The things you’re stumbling across here are more likely related to how things are done with Svelte 4 in regards to bound props. My point I was trying to make is that in scenarios like this, it’s actually simpler to redesign the parent -> child relationship in terms of their dataflow. Simplifying it like I showed above can make it easier to debug and reason with. Granted that this isn’t how things worked in Svelte 4, but that’s actually a design flaw in Svelte 4 and something we’ve been actively trying to tackle. For the cases where you need to have to do this, we offer untrack – however, I didn’t see the need in your case.

If you use untrack then you can simplify the above solution a bit:

	$effect(() => {
		if (disabled || !value) value = []
	});
	$effect(() => {
		value = filtered_items.filter(
			item => !item.disabled && untrack(() => value).includes(item.value)
		).map(
			item => item.value
		)
	})

As shown here.

The other issue is passing in undefined for the value. Instead, an array value should always be passed in, especially if it’s expected at the other side. Defaulting a bound value from undefined to something else in a child component to the parent again is just really difficult to understand and a cause for water-falling of updates which causes jank.

I’d even consider breaking out this logic further – having the filtering of the array in the parent that uses a common utility function that can do this for you. Then you likely won’t need so much indirection at all. The trick here is to not think of these patterns with a Svelte 4 mind, but to look at the problem differently given the new primitives you have in Svelte 5.

Something like this