Chaining a $derived that depends on an awaited remote function in the script tag may cause it to rerun (practically) infinitely
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 20.8k
- Forks
- 2.3k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 156
Description
Describe the bug
Say you have something like:
<script lang="ts">
import { getData, subData } from '$lib/rem/data.remote';
const mappedData = $derived(mapData(await getData({ dummyFilters: '' })));
function mapData(d: { test: string }[]) {
console.log('mapping ran');
return d.map((item) => ({ ...item, mapped: true }));
}
</script>
<form {...subData}>
<input {...subData.fields.fData.as('text')}/>
<button type="submit">Submit</button>
</form>
<ul>
{#each mappedData as item}
<li>{item.test} - {item.mapped}</li>
{/each}
</ul>
Straightforward scenario, just a remote query that fetches some dummy data, then the page displays it in a list. The data is mapped before it is rendered and awaited directly in the script tag (although in my project it was in a @const inside a boundary, the basics of this are still the same).
Clicking on the button submits the form. Once the form finishes, the data is updated. In my repro the form does nothing as it's irrelevant to the issue. If you, however, look in the console, you'll notice that "mapping ran" is printed two times. Kind of weird, I'd expect it to run only once, but I just chalked it up to "Svelte magic" and didn't question it. (In my real project, it actually ran like 5 or 6 times, I guess it depends on the rows it needs to render?)
Now here's the issue. Literally the same code, except we extract the remote function call into a separate variable because we're responsible software engineers and are trying to keep the code nice to look at:
<script lang="ts">
import { getData, subData } from '$lib/rem/data.remote';
const data = $derived(await getData({ dummyFilters: '' }));
const mappedData = $derived(mapData(data));
//everything else identical
</script>
Click the button again, now it runs 5-6 times. Why is this a problem? Because in my real app (where I have a lot of rendered components inside a table, with popovers over some columns and rows, and the returned data is itself part of other deriveds in the table component, etc) it's more like 60.000 re-runs of the mapping function, effectively freezing the page for a good 10 seconds.
But wait, there's more! Let's add a $inspect('data, data). The data only appears once, inspecting it shows it isn't actually being marked as stale to warrant all those function calls.
Ok, whatever, maybe it's a quirk of $inspect. But let's make this even weirder:
$inspect('data', data)
$inspect('mappedData', mappedData)
Add inspects over both the props, click the button and... no more error. Console log runs only once, as it's supposed to. Except it's still buggy because, even though in the repro console log nothing happens, in my real app i get a whole bunch of that "derived is disconnected, stale data" warning in the console, so clearly the underlying issue is somewhere even deeper.
Reproduction
https://github.com/big-mike-88/async-derived-error
Click the button, watch the console logs.
Logs
System Info
System:
OS: macOS 26.5
CPU: (11) arm64 Apple M3 Pro
Memory: 124.83 MB / 18.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 26.0.0 - /opt/homebrew/bin/node
npm: 11.12.1 - /opt/homebrew/bin/npm
bun: 1.3.14 - /opt/homebrew/bin/bun
Deno: 2.8.1 - /opt/homebrew/bin/deno
Browsers:
Safari: 26.5
npmPackages:
@sveltejs/adapter-node: ^5.5.5 => 5.5.5
@sveltejs/kit: next => 2.66.0
@sveltejs/vite-plugin-svelte: ^7.1.2 => 7.1.2
svelte: ^5.56.1 => 5.56.3
vite: ^8.0.16 => 8.0.16
Severity
serious, but I can work around it
Additional Information
Possibly related to https://github.com/sveltejs/svelte/issues/18355 . It is very similar to what I'm experiencing, even down to the fact that if i remove the {#each} loop the function fires twice instead of 6 times (which honestly is still kind of weird). Although I don't do any async work inside the loop itself, nor do i launch any batches. So maybe this is a separate thing.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the linked reproduction repository and the Svelte component using the script-tag $derived values, then reproduce the button submission while watching the mapping logs. Trace the interaction between awaited remote functions, separated $derived values, and rendered each blocks; done means the mapping does not rerun excessively after submission and a regression test covers the behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100