Skip to content

Infinite recursion in $effect #15568

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
elpres opened this issue Mar 21, 2025 · 10 comments
Closed

Infinite recursion in $effect #15568

elpres opened this issue Mar 21, 2025 · 10 comments

Comments

@elpres
Copy link

elpres commented Mar 21, 2025

Describe the bug

The effect in the following code is looping indefinitely:

<script>
	let x = $state(0);
	let y = $state({a: 0});
	$effect(() => setY(x));

	function setY(newX){
		y = {a: 1};
		console.log($state.snapshot(y));
	};
</script>

Converting the first line in the function to y.a = 1 fixes the problem. Commenting out the second line (the one with console.log
) also does.

Reproduction

REPL

Logs

System Info

Tested with Svelte 5.22.5 (locally) and 5.23.2 (REPL)

Severity

blocking all usage of svelte

@brunnerh
Copy link
Member

I suspect that this is the expected behavior because you are reading and writing the same state in an effect, which should generally be avoided.

You can prevent y from becoming a dependency by using untrack or using the value independently from the state variable.

import { untrack } from 'svelte';
console.log(untrack(() => $state.snapshot(y)));

or

function setY(newX){
	const newValue = { a: 1 };
	y = newValue;
	console.log(newValue);
};

@paoloricciuti
Copy link
Member

Correct, this is expected...thanks for reporting tho

@paoloricciuti paoloricciuti closed this as not planned Won't fix, can't repro, duplicate, stale Mar 21, 2025
@elpres
Copy link
Author

elpres commented Mar 21, 2025

@brunnerh Thanks for you reply, and I agree, your explanation does make logical sense. But to me, turning a benign function into an infinite-loop-footgun just by adding a console.log of the variable you just assigned is rather brittle, confusing, and definitely not great DX. It would be great if cases like these were handled without blowing up. But thanks again for explaining how to work around this issue, as this is not the first time I'm running into it.

@brunnerh
Copy link
Member

brunnerh commented Mar 21, 2025

Effects should be used very sparingly and calling functions from them is inherently risky if their contents interact with state or do something outside your control. When setting one state in response to another within an $effect, that often indicates that $derived should be used instead.

@elpres
Copy link
Author

elpres commented Mar 21, 2025

I understand that, but let me explain the actual use case where I encountered this problem: I'm writing a date picker for incomplete dates, which may only consist of year, year and month, or year, month and day. The value passed to it needs to be parsed and converted from a string into its internal representation (an array of Int). Now, based on how many parts are present, the picker needs to display years, months, or days. So the process is roughly like this:

let { value } = $props();
let internalValue = $state([0, 0, 0]);
let mode = $state('year');
$effect(() => parseValue(value));

function parseValue(){
    internalValue = // ... parse string to array of ints
    mode = modeFromInternalValue(internalValue);
}

function modeFromInternalValue(internalValue){
    return isYear(internalValue) ? 'year' : 'month';
}

This causes the infinite loop I reported. To me this code seems like the straight-forward way of updating the two $state vars whenever the value changes. I don't see how I can formulate this any differently because neither variable can be $derived because they are must also be modified from other places (when the user interacts with the control).

So the question is this: Should Svelte be able to handle code like this (which IMHO is rather basic) gracefully, without looping infinitely, or should we as developers learn work-arounds for the quirks of the reactivity system. And if sticking to work-arounds is the answer, then at least better error messages would be useful.

@paoloricciuti
Copy link
Member

There's a PR open to allow writable deriveds #15570 but for the moment you can create state in deriveds to allow for a writable derived.

const writable_der = $derived.by(()=>{
    let ret = $state({ current: my_prop });
    return ret;
});

writable_der.current = "blah";

Assigning state in effect is never a good idea, and even if you don't want to use this solution i would suggest wrapping the state in a setter so that you can update both states.

@elpres
Copy link
Author

elpres commented Mar 21, 2025

@paoloricciuti Thanks, I'll have to think about how to apply your advice, and I'll keep an eye on that PR.

@elpres
Copy link
Author

elpres commented Apr 3, 2025

@Rich-Harris This issue seems a lot like what #15553 is addressing to me, or am I misunderstanding the goal of the PR? In this issue there is also an "unsafe read" that doesn't modify any state but still causes an infinite loop. I tried running the REPL against 5.25.6 and it still loops, so the question is, is this the same kind of problem you were fixing and should the PR have fixed this issue as well?

@brunnerh
Copy link
Member

brunnerh commented Apr 3, 2025

That PR is about state created within an $effect, which is not the case here.

@falco467
Copy link

@elpres
I just wrote a similar component a few weeks ago. I think the cleanest solution for this read/write derived values is to use an object with getter and setter: let v = {get value () {...}, set value () {...}} and <input bind:value={v.value} />
With this you can create a two-way-binding of two values. If the underlying state changed, Svelte will update the value through the get-dependency and if the user edits the value it will automatically call the setter, where you can parse and update your values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants