Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

page.params from $app/state not a rune server-side #14954

Open
quentinderoubaix opened this issue Jan 7, 2025 · 3 comments
Open

page.params from $app/state not a rune server-side #14954

quentinderoubaix opened this issue Jan 7, 2025 · 3 comments

Comments

@quentinderoubaix
Copy link

Describe the bug

I don't think this is a bug, but would like to open the discussion through this issue.

We now have the possibility to use the page object from $app/state instead of the store from $app/stores, which is awesome !

Something that could be confusing however is that the page object exposes runes only in client mode, and not in server mode.
Which means that if a user writes the following code:

import {page} from '$app/state';

export const routing = new (class Routing {
    #nameUpper = $derived(page.params.name?.toUpperCase());
    get nameUpper() {
        return this.#nameUpper;
    }
})();

This will work perfectly fine client-side, but will not work for pre-rendered routes !
You can check the reproduction link to see it.

Obviously, a simple fix is updating the above code to the following:

import {page} from '$app/state';

export const routing = new (class Routing {
    get nameUpper() {
        return page.params.name?.toUpperCase();
    }
})();

So there is no bug, just a slight confusion... Should users avoid using $derived with the page info ? Should they check if running in the browser to use $derived or simple getters ?

Reproduction

https://github.com/quentinderoubaix/showcase-app-state-behavior

Logs

No response

System Info

System:
  OS: Linux 5.15 Ubuntu 24.04.1 LTS 24.04.1 LTS (Noble Numbat)
  CPU: (20) x64 12th Gen Intel(R) Core(TM) i7-1280P
  Memory: 8.45 GB / 15.47 GB
  Container: Yes
  Shell: 5.2.21 - /bin/bash
Binaries:
  Node: 22.11.0 - /usr/local/bin/node
  Yarn: 1.22.19 - /usr/local/bin/yarn
  npm: 10.9.2 - /usr/local/bin/npm
  pnpm: 9.15.3 - /usr/local/bin/pnpm
  bun: 1.1.0 - ~/.bun/bin/bun
  Watchman: 4.9.0 - /usr/bin/watchman
npmPackages:
  @sveltejs/adapter-static: ^3.0.6 => 3.0.8 
  @sveltejs/kit: ^2.0.0 => 2.15.2 
  @sveltejs/vite-plugin-svelte: ^4.0.0 => 4.0.4 
  svelte: ^5.0.0 => 5.16.5 
  vite: ^5.4.11 => 5.4.11

Severity

annoyance

Additional Information

No response

@dummdidumm
Copy link
Member

This isn't so much related to $app/state not using runes on the server, rather that $derived on the server only executes once, and then the value is stable: Under the hood it's compiled to #nameUpper = $.once(() => page.params.name?.toUpperCase());. So for your page.params from $app/state, you're retrieving the value for the Rich route first, and then the value stays that way even for the Simon route.

So yes, your solution to not use $derived is the correct approach here, though I gotta admit it's non-obvious. For stores it worked because the whole chain of subscribers did reexecute everytime a component subscribed in SSR, there was no place where it computed the value once and then not again.

@quentinderoubaix
Copy link
Author

thx for the clarification @dummdidumm !

I was under the impression that any application state managed previously through svelte stores could be migrated to $state and $derived runes. Was I incorrect ?

While the uppercase example is very simplistic, users might want to use the power of runes to combine route params or data with other $states...

import {page} from '$app/state';

export const appState = new (class AppState{
  #awesomeDevs = $state(['Rich', 'Simon']);
  #isAwesomeDev = $derived(this.#awesomeDevs.includes(page.params.name));

  get isAwesomeDev() {
      return this.#isAwesomeDev;
  }
  set awesomeDevs(devs: string[]) {
    this.#awesomeDevs = devs;
  }
})();

I am not sure how valid this kind of 'usage' is to be fair...

@Rich-Harris Rich-Harris transferred this issue from sveltejs/kit Jan 8, 2025
@Rich-Harris
Copy link
Member

Transferred this to the svelte repo. The obvious fix would be to thunkify deriveds, so that this...

<script>
  let a = $state(1);
  let b = $derived(a * 2);
</script>

<h1>{a} * 2 = {b}</h1>

...becomes this:

export default function App($$payload) {
  let a = 1;
- let b = a * 2;
+ let b = () => a * 2;

- $$payload.out += `<h1>${$.escape(a)} * 2 = ${$.escape(b)}</h1>`;
+ $$payload.out += `<h1>${$.escape(a)} * 2 = ${$.escape(b())}</h1>`;
}

Meanwhile class deriveds, like the one above, would change thusly (i.e. just removing the $.once):

// input
class Routing {
  nameUpper = $derived(page.params.name?.toUpperCase())
}
// output
class Routing {
- #nameUpper = $.once(() => page.params.name?.toUpperCase());
+ #nameUpper = () => page.params.name?.toUpperCase();

  get nameUpper() {
    return this.#nameUpper();
  }
}

Two obvious downsides:

  • it's more work
  • there's no caching — it will re-evaluate expensive deriveds

Maybe a compromise that solves the case at hand without causing extra work in the majority of cases would be to distinguish between deriveds that belong to a component instance (which shouldn't change during rendering, unless you're doing inadvisable things) and those that don't.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants