Skip to content

SvelteKit 2 shallow routing: page.state updated by pushState/replaceState, but not page.url

TL;DR.

Writing filter/sort params to the address bar with replaceState(url, state) does not change $page.url (or page.url in Svelte 5). Own the state locally, initialize it once from $page.url.searchParams (SSR-safe), and treat replaceState as write-only.

Symptom

A client-side filter writes ?show=waiting with replaceState('/home?show=waiting', $page.state) from $app/navigation, and a reactive $: view = parse($page.url.searchParams) never changes. The address bar shows the new URL; the store does not.

Cause

Verified in @sveltejs/kit 2.x src/runtime/client/client.js (replaceState, ~line 2551; pushState ~2504): both call history.replaceState/pushState(opts, '', resolve_url(url)), then set page.state = state and re-render the root with clone_page(page). page.url is only reassigned by update_url() inside real navigations. Shallow routing intentionally keeps page.url pinned to the last navigated URL; the new location is stored in history state under PAGE_URL_KEY.

Fix

  • Keep the UI state as a local variable, initialized once from $page.url.searchParams (the server renders with the same URL, so first paint already reflects a shared/reloaded link; no flicker).
  • On user action: mutate the local state, then replaceState(path + search, $page.state) as a one-way mirror.
  • Don't use goto(url, { replaceState: true }) for this if the page has a server load: it re-runs load (network round trip) on every tap.
  • replaceState throws in dev before the router starts ("Cannot call replaceState(...) before router is initialized"); calling it from a click handler is safe, from onMount on a hard load it needs a macrotask (setTimeout).

Applies to SvelteKit 2 shallow routing (pushState/replaceState from $app/navigation).

No signals yet