kit icon indicating copy to clipboard operation
kit copied to clipboard

`$page` store doesn't update when you set search params in the URL using `history`

Open jhwheeler opened this issue 2 years ago • 12 comments

Describe the bug

When manually setting the query parameters in the URL using history, the $page store does not update correctly to reflect these changes.

In my SvelteKit application, I have a function that updates the query parameters in the URL using the history API. However, after updating the query parameters, the $page store does not reflect these changes. This behavior is inconsistent with the expected behavior, where the $page store should update to reflect the current state of the page, including the query parameters.

I also attempted to use goto to work around this, but the issue in my application is that the URL search params are being updated from a search input, and when you goto, the focus is lost on the input. Manually handling refocusing the input is possible, but quite the hassle.

My current workaround is to not rely on $page.url, and only use window.location.

Reproduction

Here is a repo you can clone so you can reproduce the issue: https://github.com/jhwheeler/sveltekit-search-params-issue

In this example, when you click the "Update Query Params" button, it updates the query parameters in the URL using the history API. However, the $page store does not update correctly to reflect these changes:

<script>
  import { page } from '$app/stores'
  import { browser } from '$app/environment'

  let queryParams = ''

  function updateQueryParams() {
    queryParams = 'newParams'

    const params = new URLSearchParams(window.location.search)

    params.set('q', queryParams)

    history.replaceState(
      history.state,
      '',
      decodeURIComponent(`${window.location.pathname}?${params}`),
    )
  }

  $: currentSearchParams = browser ? Object.fromEntries($page.url.searchParams) : null

  $: console.log('currentSearchParams', currentSearchParams)
</script>

<button on:click={updateQueryParams}>Update Query Params</button>
<p>Query Params: {queryParams}</p>

{#if browser}
  <p>currentSearchParams: {JSON.stringify(currentSearchParams)}</p>
{/if}

Here is a GIF of the output:

CleanShot 2023-08-31 at 21 24 44

System Info

System:
    OS: macOS 13.2.1
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 2.10 GB / 16.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 18.12.0 - ~/.nvm/versions/node/v18.12.0/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 8.19.2 - ~/.nvm/versions/node/v18.12.0/bin/npm
  Browsers:
    Brave Browser: 116.1.57.57
    Chrome: 116.0.5845.140
    Edge: 113.0.1774.57
    Safari: 16.3
  npmPackages:
    @sveltejs/adapter-auto: ^2.0.0 => 2.1.0
    @sveltejs/kit: ^1.20.4 => 1.24.0
    svelte: ^4.0.5 => 4.2.0
    vite: ^4.4.2 => 4.4.9

Severity

serious, but I can work around it

jhwheeler avatar Aug 31 '23 13:08 jhwheeler

edit: I mis read that you already used goto but will leave this answer here in case someone else finds it helpful

You can use the 'goto' function provided by Sveltekit to handle this.

  import { goto } from "$app/navigation";

  function updateQueryParams() {
    queryParams = 'newParams'

    const params = new URLSearchParams(window.location.search)

    params.set('q', queryParams)

    goto(`?${params.toString()}`);
  }

A better way of achieving this is to use an 'a' tag instead of a button. (you can style it to look like a button or just wrap the button in an 'a' tag and remove your click listener). This has some accessibility benefits and makes it easier to understand whats going on when you come back to the code in the future. and should also work if javascript is disabled.

<script>
  import { page } from '$app/stores'
  import { browser } from '$app/environment'

  let queryParams = ''

  function updateQueryParams() {
    queryParams = 'newParams'

    const params = new URLSearchParams(window.location.search)

    params.set('q', queryParams)

   return `?${params.toString()}`
  }

  $: currentSearchParams = browser ? Object.fromEntries($page.url.searchParams) : null

  $: console.log('currentSearchParams', currentSearchParams)
</script>

<a href={updateQueryParams()}>Update Query Params</a>
<p>Query Params: {queryParams}</p>

{#if browser}
  <p>currentSearchParams: {JSON.stringify(currentSearchParams)}</p>
{/if}

alexvdvalk avatar Sep 05 '23 09:09 alexvdvalk

You can use goto with keepfocus set to true:

<script>
  import { page } from "$app/stores";
  import { browser } from "$app/environment";
  import { goto } from "$app/navigation";

  let queryParams = "";

  function updateQueryParams(e) {
    const params = new URLSearchParams($page.url.searchParams);

    params.set("q", e.target.value);

    goto(`?${params.toString()}`, { keepFocus: true });
  }
  $: currentSearchParams = browser
    ? Object.fromEntries($page.url.searchParams)
    : null;

  $: console.log("currentSearchParams", currentSearchParams);
</script>

<!-- <button on:click={updateQueryParams}>Update Query Params</button> -->
<input type="text" on:input={updateQueryParams} />
<p>Query Params: {queryParams}</p>

{#if browser}
  <p>currentSearchParams: {JSON.stringify(currentSearchParams)}</p>
{/if}

alexvdvalk avatar Sep 05 '23 10:09 alexvdvalk

The issue is that we cannot detect changes made by history.replaceState() unless we proxy it (then we can update $page.url). We should decide if we want to support the History API better or improve the documentation of the existing solutions (with a focus on people using goto instead).

or using data-sveltekit-keepfocus:

<!-- SvelteKit enhances the form behaviour with a client-side navigation for GET method forms -->
<form data-sveltekit-keepfocus>
    <!-- Natively populates the URL search params with the input name and value-->
	<input type="text" name="q" />
</form>

https://kit.svelte.dev/docs/link-options#data-sveltekit-keepfocus

Below is an example with both goto and the form solution implemented.

https://stackblitz.com/edit/github-m927tu?file=src%2Froutes%2F%2Bpage.svelte,package.json,src%2Froutes%2F%2Bpage.js

teemingc avatar Oct 10 '23 12:10 teemingc

I don't think goto is the solution. I currently use this with a "?q=" parameter to keep what the user is searching. The idea is doing this so if they go back or to the results, the search is kept the same. The issue with goto in this case is that ig user wants to go back, it will start deleting letter by letter on the q parameter (it won't update the textbox tho).

Will there be any solution to handle the history api? Or maybe I can update it manually. Thank you

My website is: soundicly.com

polvallverdu avatar Dec 12 '23 19:12 polvallverdu

I'm currently using goto as mentioned, but I just have one issue with it. I'm listening for afterNavigation to update stuff, but when I press the back arrow until getting to / (without parameter), nothing triggers (not mount, beforeNavigation, afterNavegation)

polvallverdu avatar Dec 13 '23 23:12 polvallverdu

The issue is that we cannot detect changes made by history.replaceState() unless we proxy it (then we can update $page.url). We should decide if we want to support the History API better or improve the documentation of the existing solutions (with a focus on people using goto instead).

With the new pushState and replaceState "proxies", this should be possible though? But even when using the SvelteKit pushState or replaceState, the $page.url won't update.

bummzack avatar Aug 14 '24 06:08 bummzack

But even when using the SvelteKit pushState or replaceState, the $page.url won't update.

This can be reproduced, does not match the description in the documentation

"retrieving the page state that was set through goto, pushState or replaceState (also see goto and shallow routing)"

NeroBlackstone avatar Mar 25 '25 16:03 NeroBlackstone

Simple code for reproduce:

+page.svelte:

<script>
    import { page } from '$app/state';
    import { replaceState } from '$app/navigation';
</script>

{page.url}
<br>
<button onclick={()=>replaceState("/2025",{})}>replaceState</button>

Before click button:

Image

After click button:

Image

Obviously this behavior is inconsistent with the documentation, is this a bug? I really hope someone can answer this question, what is the way to update the page state while replaceState?

NeroBlackstone avatar Mar 25 '25 17:03 NeroBlackstone

hi, i had similar case, navigation event is fired but never resolved. tried await goto('?type=profile-select') (with await) and it worked, worth giving it a try if is anyone having this issue ig.

stabldev avatar Mar 30 '25 13:03 stabldev

@NeroBlackstone

"retrieving the page state that was set [...]"

means page.state.

What you probably want is goto('/2025', { replaceState: true }).

PatrickG avatar Aug 06 '25 16:08 PatrickG

I'll add that this issue also applies to the URL hash, not only to search params. URL updates like replaceState('/#myhash', {}) are also not reflected in $page.url.hash. The goto workaround mentioned above fixes this in my case.

th0rgall avatar Sep 10 '25 14:09 th0rgall