DOM reverts to pre-navigation render when same-route `goto({invalidateAll: true})` runs while a `$state`-bound attribute toggles around the await
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
After clicking a button that performs goto('/new', { invalidateAll: true }) to the same parameterized route (/fundraisers/[id]), the URL advances and reactive state updates to the new data (verified via $inspect / $effect), and the DOM briefly renders the new content. Then, within ~50 ms, the DOM is rolled back to the previous render and stays there. No further reactivity recovers it.
See Trigger Set below for all parts needed to reproduce
Trigger set (all required to reproduce)
Each of these was individually bisected; removing any of them masks the bug:
compilerOptions.experimental.async: true.- Same-route
goto(url, { invalidateAll: true })where the URL changes a route param. - A click handler that mutates a
$statetotruebeforeawait goto(...), and back tofalsein afinallyblock after it resolves. - A DOM attribute bound to that
$state(e.g.disabled={markingReviewed}) on an element inside a control-flow block that gets torn down and rebuilt when the route data changes —{#key fundraiser.id}is the simplest form, but{#if condition-derived-from-data}…{:else}…{/if}also reproduces. Removing the wrapping block (so the button is a stable DOM node across navigation) fixes it. - Elsewhere on the page: a child component rendered via
{#each}that contains a<svelte:boundary>with a top-levelawait(the awaited promise can reject; behavior is the same onpending → failed).
Notable details:
- Inlining the
<svelte:boundary>directly into+page.svelte(no child component) does not reproduce — it must be one component deep. - Replacing the click handler with a
<form action="?/x" use:enhance>that runs the same logic reproduces too. - Removing the
{#key}block — even keeping thedisabled={markingReviewed}button — does not reproduce. - Removing
disabled={markingReviewed}while keeping the{#key}block does not reproduce. - Hover preload (
data-sveltekit-preload-data="hover") is not needed (this distinguishes it from sveltejs/kit#14923, which has a similar "flash then revert" signature but different trigger). - A
$effectreading the post-navigation state fires correctly with the new value at the moment of the revert, so reactivity itself isn't broken — only DOM commit is rolling back.
What Claude thinks is happening
This looks like Svelte 5 async-coherence is treating the new render as not-yet-coherent because the new boundary's pending await is still in flight, and rolling the DOM back to the "previous coherent state" — but it never advances forward again after the boundary settles (failed snippet renders in the boundary fine; the parent DOM stays at the previous render).
The disabled={markingReviewed} binding seems to be what wires the click handler's await into the same coherence frame as the navigation, since removing it (or hoisting it out of the {#key} block) breaks the trigger. I suspect {#key} participates because it forces the new render to be a fresh subtree rather than a structural diff.
Reproduction
See this repository:
https://github.com/Narretz/svelte-async-revert
Logs
System Info
## Versions
Verified on the latest of everything as of 2026-05-19:
- `svelte`: 5.55.8
- `@sveltejs/kit`: 2.60.1
- `vite`: 8.0.13
- `@sveltejs/vite-plugin-svelte`: 7.1.2
Severity
serious, but I can work around it
Additional Information
No response
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 by running the linked svelte-async-revert reproduction against the listed SvelteKit, Svelte, Vite, and plugin versions, then verify the full trigger set, especially the keyed block, bound attribute, same-route goto, and nested boundary. Trace the navigation and async-boundary behavior until the DOM rollback is explained; done means the new route content remains rendered after navigation and boundary settlement.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- 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