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)
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:
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.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 😃
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
$effectbut 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.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
$effectrune 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.
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:
@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
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
$effectneeds 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
untrackthen you can simplify the above solution a bit:As shown here.
The other issue is passing in
undefinedfor the value. Instead, an array value should always be passed in, especially if it’s expected at the other side. Defaulting a bound value fromundefinedto 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