Describe the bug
invalidate() or invalidateAll() can run while a client-side navigation is still loading. That navigation is then aborted, but navigating is never reset. It keeps holding the aborted navigation until another navigation completes, so anything that shows a loading state from navigating stays in that state.
From packages/kit/src/runtime/client/client.js in 2.70.3:
navigate() sets stores.navigating / navigating.current when the navigation starts (:1758).
_invalidate() takes a new token (:425, const nav_token = (token = {})).
- When the navigation's load returns,
navigate() sees that the token has changed (:1809). It calls nav.reject(new Error('navigation aborted')) and returns.
- The only reset of
navigating for a navigation happens at the end of one that completes (:2050), and that early return skips it. :1988 is a second early return on the same token check.
Reproduction
A fresh npx sv create --template minimal --types ts app, with @sveltejs/kit 2.70.3 and svelte 5.57.0.
src/routes/+page.ts:
export async function load({ url }) {
if (url.searchParams.has('slow')) await new Promise((r) => setTimeout(r, 1000));
return {};
}
src/routes/+page.svelte:
<script lang="ts">
import { goto, invalidateAll } from '$app/navigation';
import { navigating } from '$app/state';
function go() {
goto('?slow=1').catch(() => {});
setTimeout(() => invalidateAll(), 100);
}
function goPlain() {
goto('?slow=1').catch(() => {});
}
</script>
<button id="go" onclick={go}>go</button>
<button id="plain" onclick={goPlain}>go without invalidate</button>
<p id="state">navigating: {navigating.to ? 'yes' : 'no'}</p>
Clicking each button in Chromium (driven by Playwright, reading #state 200 ms after the click and again 3 s later) gives:
| button |
200 ms after the click |
3 s later |
URL afterwards |
| go without invalidate (control) |
navigating: yes |
navigating: no |
?slow=1 |
go (invalidateAll 100 ms after goto) |
navigating: yes |
navigating: yes |
unchanged |
The aborted navigation's URL is never applied, which seems right. The problem is that navigating stays set indefinitely, until some later navigation completes.
We met this in an app where a search box calls goto on a debounce while a form's invalidateAll, after a save, lands inside the search's load. The app's loading overlay reads navigating, so it stayed up for good.
Expected behaviour
navigating goes back to null when a navigation is aborted, just as it does when one completes. Alternatively, whatever superseded the navigation could take the store over.
Workaround
Treat a navigation as over once its complete promise settles, either way:
let ended = $state.raw(null); // raw: the store holds the object itself, and a proxy is never identical to it
$effect(() => {
const n = $navigating;
if (n) n.complete.then(() => (ended = n), () => (ended = n));
});
const isNavigating = $derived($navigating !== null && $navigating !== ended);
Related
#13778 (aborted navigations fail silently) is the same abort path, seen from the side of whoever called goto.
System Info
@sveltejs/kit 2.70.3, svelte 5.57.0, the Vite version from the minimal template, Chromium via Playwright, macOS. By reading the code, the same early return exists in 2.59.1, but I have not run it there.
Severity
annoying, but we have a workaround
Describe the bug
invalidate()orinvalidateAll()can run while a client-side navigation is still loading. That navigation is then aborted, butnavigatingis never reset. It keeps holding the aborted navigation until another navigation completes, so anything that shows a loading state fromnavigatingstays in that state.From
packages/kit/src/runtime/client/client.jsin 2.70.3:navigate()setsstores.navigating/navigating.currentwhen the navigation starts (:1758)._invalidate()takes a new token (:425,const nav_token = (token = {})).navigate()sees that the token has changed (:1809). It callsnav.reject(new Error('navigation aborted'))and returns.navigatingfor a navigation happens at the end of one that completes (:2050), and that early return skips it.:1988is a second early return on the same token check.Reproduction
A fresh
npx sv create --template minimal --types tsapp, with @sveltejs/kit 2.70.3 and svelte 5.57.0.src/routes/+page.ts:src/routes/+page.svelte:Clicking each button in Chromium (driven by Playwright, reading
#state200 ms after the click and again 3 s later) gives:navigating: yesnavigating: no?slow=1invalidateAll100 ms aftergoto)navigating: yesnavigating: yesThe aborted navigation's URL is never applied, which seems right. The problem is that
navigatingstays set indefinitely, until some later navigation completes.We met this in an app where a search box calls
gotoon a debounce while a form'sinvalidateAll, after a save, lands inside the search's load. The app's loading overlay readsnavigating, so it stayed up for good.Expected behaviour
navigatinggoes back tonullwhen a navigation is aborted, just as it does when one completes. Alternatively, whatever superseded the navigation could take the store over.Workaround
Treat a navigation as over once its
completepromise settles, either way:Related
#13778 (aborted navigations fail silently) is the same abort path, seen from the side of whoever called
goto.System Info
@sveltejs/kit 2.70.3, svelte 5.57.0, the Vite version from the minimal template, Chromium via Playwright, macOS. By reading the code, the same early return exists in 2.59.1, but I have not run it there.
Severity
annoying, but we have a workaround