sveltejs/svelte

Cannot migrate `page` to `app-state` in module-level `derived`

Closed this issue · 10 comments

Describe the bug

I am finishing migrating our project to Svelte 5 and I noticed that we need to migrate page from $app/stores to $app/state.

However, this causes an issue with derived stores in a TS module. In fact, page is not Readable anymore, so, it does not trigger any reactivity.

And it cannot be completely migrated to being consumed in each components, since there are other derived depending on the first derived, and so on and so forth.

It seems to be kind of a feature loss, or maybe is it an unexpected usage? What is the official migration guide here?

Note

I had raised the same issue with the migration script, but it was ultimately dismissed without a proper solution or guide. See #16758

Reproduction

Before

// ./nav.derived.ts

import { page } from '$app/stores';
import { derived, type Readable } from 'svelte/store';

export const routeCollection: Readable<string | undefined | null}> = derived(
	[page],
	([
		{
			data: { collection }
		}
	]) => collection
);

After

// ./nav.derived.ts

import { page } from '$app/state';
import { derived, type Readable } from 'svelte/store';

export const routeCollection: Readable<string | undefined | null}> = derived(
	[page], // <---- Property subscribe is missing in type [...] but required in type Readable<any>
	([
		{
			data: { collection }
		}
	]) => collection
);

System Info

System:
    OS: macOS 26.0.1
    CPU: (11) arm64 Apple M3 Pro
    Memory: 390.48 MB / 36.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.17.0 - /usr/local/bin/node
    Yarn: 1.22.22 - /usr/local/bin/yarn
    npm: 10.9.2 - /usr/local/bin/npm
    pnpm: 10.12.1 - /usr/local/bin/pnpm
  Browsers:
    Chrome: 142.0.7444.60
    Firefox: 144.0.2
    Safari: 26.0.1
  npmPackages:
    svelte: ^5.41.0 => 5.41.0

Severity

blocking an upgrade

derived() only works with Svelte 4 stores. It does not work with Svelte 5 $state proxies. Use the $derived() rune instead.

In this case:

const routeCollection = $derived(page.data.collection);

Keep in mind that page.data, unlike $props whose types are generated automatically, needs to be manually typed in app.d.ts and that each property should be optional. Props have better type safety:

const { data } = $props();

Thank you for the suggestion @sillvva !

However, as specified, the usage I am referring to is not inside a Svelte component, but in a TS module using derived (not rune $derived that cannot be used outside a Svelte component).

Since it is still something that should still work (unless there was an official communication that I missed), I am wondering how I should migrate. I am pretty sure it is not a workaround but a common pattern to use the old page store outside a Svelte component.

As mentioned, it is not a good migration to move all the derived stores inside each component, since it becomes quite an unmanageable code with a lot of repeated code.

Or am I completely wrong and $derived rune can be used in a TS module?

The clear workaround is to create some external writable stores. Then in a new component, create an $effect that updates such stores based on page.

But that is a workaround, not really an official solution...

You can use stores in $derived and $effect runes, but you can't use $state in derived() or similar.

Your best bet at that point is to export a function that returns the value.

export function getRouteCollection() {
  return page.data.collection;
}

right! but the problem is that i have other derived stores based on those derived, and so on. So, it is a gigantic refactoring that basically transforms a lot of derived stores into functions, removing the reactivity that is required.

So, I really think that something is a bit strange: what would be the correct migration suggestion for my use-case, so that reactivity is kept and it is still in a TS module?

// ./nav.derived.ts

import { page } from '$app/stores';
import { derived, type Readable } from 'svelte/store';

export const routeCollection: Readable<string | undefined | null}> = derived(
	[page],
	([
		{
			data: { collection }
		}
	]) => collection
);

If you want to keep the store contract for now you can use https://svelte.dev/docs/svelte/svelte-store#toStore

Else keep using the page store in that part of the app until you're ready to convert. You can also start at the leafs using https://svelte.dev/docs/svelte/svelte-store#fromStore

Closing since this is not a bug or feature request, but feel free to ask clarifying questions

Thank you @dummdidumm, but the first one (toStore) seems more of a workaround, and not really efficient.

Else keep using the page store in that part of the app until you're ready to convert.

But my question was exactly that: how do I migrate? What is the official guide? Until now, it is just workarounds. And for sure the solution is not to put page inside all the components, since it will create a lot of code-repetitions.

Please re-open this issue, because it really seems that there is no correct plan for the migration of page for this use-case: so either there is a bug for the migration, or I am requesting a new feature to keep page reactive in the TS modules.

What about toStore is not efficient or sufficient? You said that you don't want to migrate your other stores, so toStore will transform the page state into a store. Or you start at the leafs of your stores and transform them to $state/$derived and using fromStore to turn stores into reactive objects you can use within them.
Either way you gotta start somewhere and then migrate, either bit by bit using those helpers or all at once.

Maybe I misunderstood how to use toStore: how would you use it in this case? Something like this?

// ./nav.derived.ts

import { page } from '$app/state';
import type { OptionString } from '$lib/types/string';
import { derived, toStore, type Readable } from 'svelte/store';

const pageStore = toStore(() => page);

export const routeCollection: Readable<string | undefined | null}> = derived(
	[pageStore],
	([
		{
			data: { collection }
		}
	]) => collection
);

I tried it and it is not triggering any reaction in routeCollection when I change page parameters for example. Maybe I am doing something wrong?

Either way you gotta start somewhere and then migrate, either bit by bit using those helpers or all at once.

So, the official migration guide for this use-case is to put page in each component? Even when I have nested derived stores depending on it, meaning a lot of repeated code?

Again, I am not against solutions, I am just asking for guidance here, since there is nothing in the official migration guide about this case: the decision was taken to deprecate one in favor of the other, so I imagine they thought of all possible use-cases.

P.S. a quick search will show you who is applying my same usage.