diff --git a/.changeset/shallow-routing-initial-effect.md b/.changeset/shallow-routing-initial-effect.md new file mode 100644 index 000000000000..2f28cba13fab --- /dev/null +++ b/.changeset/shallow-routing-initial-effect.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: allow `pushState` and `replaceState` to be called during initial render diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index c02e1274509d..075a88603de6 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -278,6 +278,8 @@ let current = { /** this being true means we SSR'd */ let hydrated = false; let started = false; +/** True once shallow routing can be used, which is before `started` since effects run during initial render (#15927) */ +let router_initialized = false; let autoscroll = true; let updating = false; let is_navigating = false; @@ -719,6 +721,10 @@ async function initialize(result, target, hydrate) { update(/** @type {import('@sveltejs/kit').Page} */ (result.props.page)); current_tree = result.props.tree; + // effects flush synchronously while the root component is created below, + // so anything shallow routing needs must be ready before this point + router_initialized = true; + // TODO: use mount() root = new Root({ target, @@ -2572,7 +2578,7 @@ export function pushState(url, state) { } if (DEV) { - if (!started) { + if (!router_initialized) { throw new Error('Cannot call pushState(...) before router is initialized'); } @@ -2590,7 +2596,8 @@ export function pushState(url, state) { const opts = { [HISTORY_INDEX]: (current_history_index += 1), [NAVIGATION_INDEX]: current_navigation_index, - [PAGE_URL_KEY]: page.url.href, + // untracked so that effects calling this function don't depend on `page.url` + [PAGE_URL_KEY]: svelte.untrack(() => page.url.href), [STATES_KEY]: state }; @@ -2615,7 +2622,7 @@ export function replaceState(url, state) { } if (DEV) { - if (!started) { + if (!router_initialized) { throw new Error('Cannot call replaceState(...) before router is initialized'); } @@ -2631,7 +2638,8 @@ export function replaceState(url, state) { const opts = { [HISTORY_INDEX]: current_history_index, [NAVIGATION_INDEX]: current_navigation_index, - [PAGE_URL_KEY]: page.url.href, + // untracked so that effects calling this function don't depend on `page.url` + [PAGE_URL_KEY]: svelte.untrack(() => page.url.href), [STATES_KEY]: state }; diff --git a/packages/kit/test/apps/basics/src/app.d.ts b/packages/kit/test/apps/basics/src/app.d.ts index e3198c7bd1c7..3fb8f648b0d8 100644 --- a/packages/kit/test/apps/basics/src/app.d.ts +++ b/packages/kit/test/apps/basics/src/app.d.ts @@ -12,6 +12,7 @@ declare global { interface PageState { active?: boolean; count?: number; + initial?: boolean; } } } diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/initial-effect/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/initial-effect/+page.svelte new file mode 100644 index 000000000000..8204f6744594 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/initial-effect/+page.svelte @@ -0,0 +1,10 @@ + + +
initial: {page.state.initial ?? false}
diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/initial-effect/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/initial-effect/+page.svelte new file mode 100644 index 000000000000..2669ad537773 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/initial-effect/+page.svelte @@ -0,0 +1,10 @@ + + +initial: {page.state.initial ?? false}
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 143244dcba84..776d99379112 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -1801,6 +1801,19 @@ test.describe('Shallow routing', () => { await expect(page.locator('p')).toHaveText('active: false'); await expect(page.locator('span')).not.toHaveText(now); }); + + test('pushState can be called in $effect during initial render', async ({ page }) => { + await page.goto('/shallow-routing/push-state/initial-effect'); + await expect(page.locator('p')).toHaveText('initial: true'); + + await page.goBack(); + await expect(page.locator('p')).toHaveText('initial: false'); + }); + + test('replaceState can be called in $effect during initial render', async ({ page }) => { + await page.goto('/shallow-routing/replace-state/initial-effect'); + await expect(page.locator('p')).toHaveText('initial: true'); + }); }); test.describe('reroute', () => {