diff --git a/.changeset/modern-news-repeat.md b/.changeset/modern-news-repeat.md new file mode 100644 index 000000000000..406afc81b895 --- /dev/null +++ b/.changeset/modern-news-repeat.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `$app/state` module diff --git a/documentation/docs/10-getting-started/40-web-standards.md b/documentation/docs/10-getting-started/40-web-standards.md index f31f4617fdb1..1e6afdf4af1a 100644 --- a/documentation/docs/10-getting-started/40-web-standards.md +++ b/documentation/docs/10-getting-started/40-web-standards.md @@ -78,7 +78,7 @@ Most of the time, your endpoints will return complete data, as in the `userAgent ## URL APIs -URLs are represented by the [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) interface, which includes useful properties like `origin` and `pathname` (and, in the browser, `hash`). This interface shows up in various places — `event.url` in [hooks](hooks) and [server routes](routing#server), [`$page.url`]($app-stores) in [pages](routing#page), `from` and `to` in [`beforeNavigate` and `afterNavigate`]($app-navigation) and so on. +URLs are represented by the [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) interface, which includes useful properties like `origin` and `pathname` (and, in the browser, `hash`). This interface shows up in various places — `event.url` in [hooks](hooks) and [server routes](routing#server), [`page.url`]($app-state) in [pages](routing#page), `from` and `to` in [`beforeNavigate` and `afterNavigate`]($app-navigation) and so on. ### URLSearchParams diff --git a/documentation/docs/20-core-concepts/10-routing.md b/documentation/docs/20-core-concepts/10-routing.md index c33229f851e9..836b93221aab 100644 --- a/documentation/docs/20-core-concepts/10-routing.md +++ b/documentation/docs/20-core-concepts/10-routing.md @@ -132,12 +132,15 @@ If an error occurs during `load`, SvelteKit will render a default error page. Yo ```svelte -

{$page.status}: {$page.error.message}

+

{page.status}: {page.error.message}

``` +> [!LEGACY] +> `$app/state` was added in SvelteKit 2.12. If you're using an earlier version or are using Svelte 4, use `$app/stores` instead. + SvelteKit will 'walk up the tree' looking for the closest error boundary — if the file above didn't exist it would try `src/routes/blog/+error.svelte` and then `src/routes/+error.svelte` before rendering the default error page. If _that_ fails (or if the error was thrown from the `load` function of the root `+layout`, which sits 'above' the root `+error`), SvelteKit will bail out and render a static fallback error page, which you can customise by creating a `src/error.html` file. If the error occurs inside a `load` function in `+layout(.server).js`, the closest error boundary in the tree is an `+error.svelte` file _above_ that layout (not next to it). diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index bba63fd2be93..269d9078dd14 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -116,14 +116,14 @@ Data returned from layout `load` functions is available to child `+layout.svelte ```svelte /// file: src/routes/blog/[slug]/+page.svelte @@ -137,24 +137,28 @@ Data returned from layout `load` functions is available to child `+layout.svelte > [!NOTE] If multiple `load` functions return data with the same key, the last one 'wins' — the result of a layout `load` returning `{ a: 1, b: 2 }` and a page `load` returning `{ b: 3, c: 4 }` would be `{ a: 1, b: 3, c: 4 }`. -## $page.data +## page.data The `+page.svelte` component, and each `+layout.svelte` component above it, has access to its own data plus all the data from its parents. -In some cases, we might need the opposite — a parent layout might need to access page data or data from a child layout. For example, the root layout might want to access a `title` property returned from a `load` function in `+page.js` or `+page.server.js`. This can be done with `$page.data`: +In some cases, we might need the opposite — a parent layout might need to access page data or data from a child layout. For example, the root layout might want to access a `title` property returned from a `load` function in `+page.js` or `+page.server.js`. This can be done with `page.data`: ```svelte - {$page.data.title} + {page.data.title} ``` -Type information for `$page.data` is provided by `App.PageData`. +Type information for `page.data` is provided by `App.PageData`. + +> [!LEGACY] +> `$app/state` was added in SvelteKit 2.12. If you're using an earlier version or are using Svelte 4, use `$app/stores` instead. +> It provides a `page` store with the same interface that you can subscribe to, e.g. `$page.data.title`. ## Universal vs server diff --git a/documentation/docs/20-core-concepts/30-form-actions.md b/documentation/docs/20-core-concepts/30-form-actions.md index 74d08c9fdc15..77bfd51fcef8 100644 --- a/documentation/docs/20-core-concepts/30-form-actions.md +++ b/documentation/docs/20-core-concepts/30-form-actions.md @@ -102,7 +102,7 @@ As well as the `action` attribute, we can use the `formaction` attribute on a bu ## Anatomy of an action -Each action receives a `RequestEvent` object, allowing you to read the data with `request.formData()`. After processing the request (for example, logging the user in by setting a cookie), the action can respond with data that will be available through the `form` property on the corresponding page and through `$page.form` app-wide until the next update. +Each action receives a `RequestEvent` object, allowing you to read the data with `request.formData()`. After processing the request (for example, logging the user in by setting a cookie), the action can respond with data that will be available through the `form` property on the corresponding page and through `page.form` app-wide until the next update. ```js /// file: src/routes/login/+page.server.js @@ -156,7 +156,7 @@ export const actions = { ### Validation errors -If the request couldn't be processed because of invalid data, you can return validation errors — along with the previously submitted form values — back to the user so that they can try again. The `fail` function lets you return an HTTP status code (typically 400 or 422, in the case of validation errors) along with the data. The status code is available through `$page.status` and the data through `form`: +If the request couldn't be processed because of invalid data, you can return validation errors — along with the previously submitted form values — back to the user so that they can try again. The `fail` function lets you return an HTTP status code (typically 400 or 422, in the case of validation errors) along with the data. The status code is available through `page.status` and the data through `form`: ```js /// file: src/routes/login/+page.server.js @@ -352,7 +352,7 @@ The easiest way to progressively enhance a form is to add the `use:enhance` acti Without an argument, `use:enhance` will emulate the browser-native behaviour, just without the full-page reloads. It will: -- update the `form` property, `$page.form` and `$page.status` on a successful or invalid response, but only if the action is on the same page you're submitting from. For example, if your form looks like `
`, `form` and `$page` will _not_ be updated. This is because in the native form submission case you would be redirected to the page the action is on. If you want to have them updated either way, use [`applyAction`](#Progressive-enhancement-Customising-use:enhance) +- update the `form` property, `page.form` and `page.status` on a successful or invalid response, but only if the action is on the same page you're submitting from. For example, if your form looks like ``, the `form` prop and the `page.form` state will _not_ be updated. This is because in the native form submission case you would be redirected to the page the action is on. If you want to have them updated either way, use [`applyAction`](#Progressive-enhancement-Customising-use:enhance) - reset the `` element - invalidate all data using `invalidateAll` on a successful response - call `goto` on a redirect response @@ -411,7 +411,7 @@ If you return a callback, you may need to reproduce part of the default `use:enh The behaviour of `applyAction(result)` depends on `result.type`: -- `success`, `failure` — sets `$page.status` to `result.status` and updates `form` and `$page.form` to `result.data` (regardless of where you are submitting from, in contrast to `update` from `enhance`) +- `success`, `failure` — sets `page.status` to `result.status` and updates `form` and `page.form` to `result.data` (regardless of where you are submitting from, in contrast to `update` from `enhance`) - `redirect` — calls `goto(result.location, { invalidateAll: true })` - `error` — renders the nearest `+error` boundary with `result.error` diff --git a/documentation/docs/20-core-concepts/50-state-management.md b/documentation/docs/20-core-concepts/50-state-management.md index be74d59535ef..465f20fc280c 100644 --- a/documentation/docs/20-core-concepts/50-state-management.md +++ b/documentation/docs/20-core-concepts/50-state-management.md @@ -40,7 +40,7 @@ Instead, you should _authenticate_ the user using [`cookies`](load#Cookies) and ## No side-effects in load -For the same reason, your `load` functions should be _pure_ — no side-effects (except maybe the occasional `console.log(...)`). For example, you might be tempted to write to a store inside a `load` function so that you can use the store value in your components: +For the same reason, your `load` functions should be _pure_ — no side-effects (except maybe the occasional `console.log(...)`). For example, you might be tempted to write to a store or global state inside a `load` function so that you can use the value in your components: ```js /// file: +page.js @@ -76,31 +76,25 @@ export async function load({ fetch }) { } ``` -...and pass it around to the components that need it, or use [`$page.data`](load#$page.data). +...and pass it around to the components that need it, or use [`page.data`](load#page.data). If you're not using SSR, then there's no risk of accidentally exposing one user's data to another. But you should still avoid side-effects in your `load` functions — your application will be much easier to reason about without them. -## Using stores with context +## Using state and stores with context -You might wonder how we're able to use `$page.data` and other [app stores]($app-stores) if we can't use our own stores. The answer is that app stores on the server use Svelte's [context API](/tutorial/svelte/context-api) — the store is attached to the component tree with `setContext`, and when you subscribe you retrieve it with `getContext`. We can do the same thing with our own stores: +You might wonder how we're able to use `page.data` and other [app state]($app-state) (or [app stores]($app-stores)) if we can't use global state. The answer is that app state and app stores on the server use Svelte's [context API](/tutorial/svelte/context-api) — the state (or store) is attached to the component tree with `setContext`, and when you subscribe you retrieve it with `getContext`. We can do the same thing with our own state: ```svelte ``` @@ -113,10 +107,15 @@ You might wonder how we're able to use `$page.data` and other [app stores]($app- const user = getContext('user'); -

Welcome {$user.name}

+

Welcome {user().name}

``` -Updating the value of a context-based store in deeper-level pages or components while the page is being rendered via SSR will not affect the value in the parent component because it has already been rendered by the time the store value is updated. In contrast, on the client (when CSR is enabled, which is the default) the value will be propagated and components, pages, and layouts higher in the hierarchy will react to the new value. Therefore, to avoid values 'flashing' during state updates during hydration, it is generally recommended to pass state down into components rather than up. +> [!NOTE] We're passing a function into `setContext` to keep reactivity across boundaries. Read more about it [here](https://svelte.dev/docs/svelte/$state#Passing-state-into-functions) + +> [!LEGACY] +> You also use stores from `svelte/store` for this, but when using Svelte 5 it is recommended to make use of universal reactivity instead. + +Updating the value of context-based state in deeper-level pages or components while the page is being rendered via SSR will not affect the value in the parent component because it has already been rendered by the time the state value is updated. In contrast, on the client (when CSR is enabled, which is the default) the value will be propagated and components, pages, and layouts higher in the hierarchy will react to the new value. Therefore, to avoid values 'flashing' during state updates during hydration, it is generally recommended to pass state down into components rather than up. If you're not using SSR (and can guarantee that you won't need to use SSR in future) then you can safely keep state in a shared module, without using the context API. @@ -163,14 +162,18 @@ Instead, we need to make the value [_reactive_](/tutorial/svelte/state): Reusing components like this means that things like sidebar scroll state are preserved, and you can easily animate between changing values. In the case that you do need to completely destroy and remount a component on navigation, you can use this pattern: ```svelte -{#key $page.url.pathname} + + +{#key page.url.pathname} {/key} ``` ## Storing state in the URL -If you have state that should survive a reload and/or affect SSR, such as filters or sorting rules on a table, URL search parameters (like `?sort=price&order=ascending`) are a good place to put them. You can put them in `` or `` attributes, or set them programmatically via `goto('?key=value')`. They can be accessed inside `load` functions via the `url` parameter, and inside components via `$page.url.searchParams`. +If you have state that should survive a reload and/or affect SSR, such as filters or sorting rules on a table, URL search parameters (like `?sort=price&order=ascending`) are a good place to put them. You can put them in `` or `` attributes, or set them programmatically via `goto('?key=value')`. They can be accessed inside `load` functions via the `url` parameter, and inside components via `page.url.searchParams`. ## Storing ephemeral state in snapshots diff --git a/documentation/docs/30-advanced/25-errors.md b/documentation/docs/30-advanced/25-errors.md index 5fd17455add9..df05e5310232 100644 --- a/documentation/docs/30-advanced/25-errors.md +++ b/documentation/docs/30-advanced/25-errors.md @@ -40,17 +40,20 @@ export async function load({ params }) { } ``` -This throws an exception that SvelteKit catches, causing it to set the response status code to 404 and render an [`+error.svelte`](routing#error) component, where `$page.error` is the object provided as the second argument to `error(...)`. +This throws an exception that SvelteKit catches, causing it to set the response status code to 404 and render an [`+error.svelte`](routing#error) component, where `page.error` is the object provided as the second argument to `error(...)`. ```svelte -

{$page.error.message}

+

{page.error.message}

``` +> [!LEGACY] +> `$app/state` was added in SvelteKit 2.12. If you're using an earlier version or are using Svelte 4, use `$app/stores` instead. + You can add extra properties to the error object if needed... ```js diff --git a/documentation/docs/30-advanced/67-shallow-routing.md b/documentation/docs/30-advanced/67-shallow-routing.md index af7d38959946..b69cee744fb1 100644 --- a/documentation/docs/30-advanced/67-shallow-routing.md +++ b/documentation/docs/30-advanced/67-shallow-routing.md @@ -12,7 +12,7 @@ SvelteKit makes this possible with the [`pushState`]($app-navigation#pushState) -{#if $page.state.showModal} +{#if page.state.showModal} history.back()} /> {/if} ``` -The modal can be dismissed by navigating back (unsetting `$page.state.showModal`) or by interacting with it in a way that causes the `close` callback to run, which will navigate back programmatically. +The modal can be dismissed by navigating back (unsetting `page.state.showModal`) or by interacting with it in a way that causes the `close` callback to run, which will navigate back programmatically. ## API The first argument to `pushState` is the URL, relative to the current URL. To stay on the current URL, use `''`. -The second argument is the new page state, which can be accessed via the [page store]($app-stores#page) as `$page.state`. You can make page state type-safe by declaring an [`App.PageState`](types#PageState) interface (usually in `src/app.d.ts`). +The second argument is the new page state, which can be accessed via the [page object]($app-state#page) as `page.state`. You can make page state type-safe by declaring an [`App.PageState`](types#PageState) interface (usually in `src/app.d.ts`). To set page state without creating a new history entry, use `replaceState` instead of `pushState`. +> [!LEGACY] +> `page.state` from `$app/state` was added in SvelteKit 2.12. If you're using an earlier version or are using Svelte 4, use `$page.state` from `$app/stores` instead. + ## Loading data for a route When shallow routing, you may want to render another `+page.svelte` inside the current page. For example, clicking on a photo thumbnail could pop up the detail view without navigating to the photo page. @@ -47,7 +50,7 @@ For this to work, you need to load the data that the `+page.svelte` expects. A c + +---{$page.data}--- ++++{page.data}+++ +``` + +Use `npx sv migrate app-state` to auto-migrate most of your `$app/stores` usages inside `.svelte` components. diff --git a/documentation/docs/60-appendix/40-migrating.md b/documentation/docs/60-appendix/40-migrating.md index 18c13db6b5ea..2fbafe307a53 100644 --- a/documentation/docs/60-appendix/40-migrating.md +++ b/documentation/docs/60-appendix/40-migrating.md @@ -115,7 +115,7 @@ const { preloading, page, session } = stores(); The `page` store still exists; `preloading` has been replaced with a `navigating` store that contains `from` and `to` properties. `page` now has `url` and `params` properties, but no `path` or `query`. -You access them differently in SvelteKit. `stores` is now `getStores`, but in most cases it is unnecessary since you can import `navigating`, and `page` directly from [`$app/stores`]($app-stores). +You access them differently in SvelteKit. `stores` is now `getStores`, but in most cases it is unnecessary since you can import `navigating`, and `page` directly from [`$app/stores`]($app-stores). If you're on Svelte 5 and SvelteKit 2.12 or higher, consider using [`$app/state`]($app-state) instead. ### Routing @@ -123,7 +123,7 @@ Regex routes are no longer supported. Instead, use [advanced route matching](adv ### Segments -Previously, layout components received a `segment` prop indicating the child segment. This has been removed; you should use the more flexible `$page.url.pathname` value to derive the segment you're interested in. +Previously, layout components received a `segment` prop indicating the child segment. This has been removed; you should use the more flexible `$page.url.pathname` (or `page.url.pathname`) value to derive the segment you're interested in. ### URLs diff --git a/documentation/docs/98-reference/20-$app-state.md b/documentation/docs/98-reference/20-$app-state.md new file mode 100644 index 000000000000..f61475fd9e15 --- /dev/null +++ b/documentation/docs/98-reference/20-$app-state.md @@ -0,0 +1,10 @@ +--- +title: $app/state +--- + +SvelteKit makes three readonly state objects available via the `$app/state` module — `page`, `navigating` and `updated`. + +> [!NOTE] +> This module was added in 2.12. If you're using an earlier version of SvelteKit, use [`$app/stores`]($app-stores) instead. + +> MODULE: $app/state diff --git a/documentation/docs/98-reference/20-$app-stores.md b/documentation/docs/98-reference/20-$app-stores.md index 0e5604f5d7ef..ff7cf8999e82 100644 --- a/documentation/docs/98-reference/20-$app-stores.md +++ b/documentation/docs/98-reference/20-$app-stores.md @@ -2,4 +2,6 @@ title: $app/stores --- +This module contains store-based equivalents of the exports from [`$app/state`]($app-state). If you're using SvelteKit 2.12 or later, use that module instead. + > MODULE: $app/stores diff --git a/packages/adapter-static/test/apps/spa/src/routes/+error.svelte b/packages/adapter-static/test/apps/spa/src/routes/+error.svelte index 9b0abbfdda47..fe313e063894 100644 --- a/packages/adapter-static/test/apps/spa/src/routes/+error.svelte +++ b/packages/adapter-static/test/apps/spa/src/routes/+error.svelte @@ -1,5 +1,5 @@ -

{$page.status}

+

{page.status}

diff --git a/packages/kit/scripts/generate-dts.js b/packages/kit/scripts/generate-dts.js index 8d21d7881d7f..c10dad802433 100644 --- a/packages/kit/scripts/generate-dts.js +++ b/packages/kit/scripts/generate-dts.js @@ -14,6 +14,7 @@ await createBundle({ '$app/navigation': 'src/runtime/app/navigation.js', '$app/paths': 'src/runtime/app/paths/types.d.ts', '$app/server': 'src/runtime/app/server/index.js', + '$app/state': 'src/runtime/app/state/client.js', '$app/stores': 'src/runtime/app/stores.js' }, include: ['src'] diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index c4ade48e09a2..4dd2d48cb5a6 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -638,17 +638,17 @@ export interface KitConfig { * /// file: +layout.svelte * * ``` * - * If you set `pollInterval` to a non-zero value, SvelteKit will poll for new versions in the background and set the value of the [`updated`](https://svelte.dev/docs/kit/$app-stores#updated) store to `true` when it detects one. + * If you set `pollInterval` to a non-zero value, SvelteKit will poll for new versions in the background and set the value of [`updated.current`](https://svelte.dev/docs/kit/$app-state#updated) `true` when it detects one. */ version?: { /** diff --git a/packages/kit/src/runtime/app/forms.js b/packages/kit/src/runtime/app/forms.js index 0c353ea3663d..ffa402489548 100644 --- a/packages/kit/src/runtime/app/forms.js +++ b/packages/kit/src/runtime/app/forms.js @@ -59,7 +59,7 @@ function clone(element) { * * If this function or its return value isn't set, it * - falls back to updating the `form` prop with the returned data if the action is on the same page as the form - * - updates `$page.status` + * - updates `page.status` * - resets the `` element and invalidates all data in case of successful submission with no redirect response * - redirects in case of a redirect response * - redirects to the nearest error page in case of an unexpected error diff --git a/packages/kit/src/runtime/app/state/client.js b/packages/kit/src/runtime/app/state/client.js new file mode 100644 index 000000000000..47d806879a3f --- /dev/null +++ b/packages/kit/src/runtime/app/state/client.js @@ -0,0 +1,84 @@ +import { + page as _page, + navigating as _navigating, + updated as _updated +} from '../../client/state.svelte.js'; +import { stores } from '../../client/client.js'; + +/** + * A reactive object with information about the current page, serving several use cases: + * - retrieving the combined `data` of all pages/layouts anywhere in your component tree (also see [loading data](https://svelte.dev/docs/kit/load)) + * - retrieving the current value of the `form` prop anywhere in your component tree (also see [form actions](https://svelte.dev/docs/kit/form-actions)) + * - retrieving the page state that was set through `goto`, `pushState` or `replaceState` (also see [goto](https://svelte.dev/docs/kit/$app-navigation#goto) and [shallow routing](https://svelte.dev/docs/kit/shallow-routing)) + * - retrieving metadata such as the URL you're on, the current route and its parameters, and whether or not there was an error + * + * ```svelte + * + * + * + *

Currently at {page.url.pathname}

+ * + * {#if page.error} + * Problem detected + * {:else} + * All systems operational + * {/if} + * ``` + * + * On the server, values can only be read during rendering (in other words _not_ in e.g. `load` functions). In the browser, the values can be read at any time. + * + * @type {import('@sveltejs/kit').Page} + */ +export const page = { + get data() { + return _page.data; + }, + get error() { + return _page.error; + }, + get form() { + return _page.form; + }, + get params() { + return _page.params; + }, + get route() { + return _page.route; + }, + get state() { + return _page.state; + }, + get status() { + return _page.status; + }, + get url() { + return _page.url; + } +}; + +/** + * An object with a reactive `current` property. + * When navigation starts, `current` is a `Navigation` object with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. + * When navigation finishes, `current` reverts to `null`. + * + * On the server, this value can only be read during rendering. In the browser, it can be read at any time. + * @type {{ get current(): import('@sveltejs/kit').Navigation | null; }} + */ +export const navigating = { + get current() { + return _navigating.current; + } +}; + +/** + * A reactive value that's initially `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update `current` to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. + * @type {{ get current(): boolean; check(): Promise; }} + */ +export const updated = { + get current() { + return _updated.current; + }, + check: stores.updated.check +}; diff --git a/packages/kit/src/runtime/app/state/package.json b/packages/kit/src/runtime/app/state/package.json new file mode 100644 index 000000000000..02450fde3884 --- /dev/null +++ b/packages/kit/src/runtime/app/state/package.json @@ -0,0 +1,9 @@ +{ + "type": "module", + "exports": { + ".": { + "browser": "./client.js", + "default": "./server.js" + } + } +} diff --git a/packages/kit/src/runtime/app/state/server.js b/packages/kit/src/runtime/app/state/server.js new file mode 100644 index 000000000000..770a85a739f8 --- /dev/null +++ b/packages/kit/src/runtime/app/state/server.js @@ -0,0 +1,60 @@ +import { getContext } from 'svelte'; + +function context() { + return getContext('__request__'); +} + +/** @param {string} name */ +function context_dev(name) { + try { + return context(); + } catch { + throw new Error( + `Can only read '${name}' on the server during rendering (not in e.g. \`load\` functions), as it is bound to the current request via component context. This prevents state from leaking between users.` + + 'For more information, see https://svelte.dev/docs/kit/state-management#avoid-shared-state-on-the-server' + ); + } +} + +// TODO we're using DEV in some places and __SVELTEKIT_DEV__ in others - why? Can we consolidate? +export const page = { + get data() { + return (__SVELTEKIT_DEV__ ? context_dev('page.data') : context()).page.data; + }, + get error() { + return (__SVELTEKIT_DEV__ ? context_dev('page.error') : context()).page.error; + }, + get form() { + return (__SVELTEKIT_DEV__ ? context_dev('page.form') : context()).page.form; + }, + get params() { + return (__SVELTEKIT_DEV__ ? context_dev('page.params') : context()).page.params; + }, + get route() { + return (__SVELTEKIT_DEV__ ? context_dev('page.route') : context()).page.route; + }, + get state() { + return (__SVELTEKIT_DEV__ ? context_dev('page.state') : context()).page.state; + }, + get status() { + return (__SVELTEKIT_DEV__ ? context_dev('page.status') : context()).page.status; + }, + get url() { + return (__SVELTEKIT_DEV__ ? context_dev('page.url') : context()).page.url; + } +}; + +export const navigating = { + get current() { + return (__SVELTEKIT_DEV__ ? context_dev('navigating.current') : context()).navigating; + } +}; + +export const updated = { + get current() { + return false; + }, + check: () => { + throw new Error('Can only call updated.check() in the browser'); + } +}; diff --git a/packages/kit/src/runtime/app/stores.js b/packages/kit/src/runtime/app/stores.js index da4b02db836e..3c4bac7e1d7b 100644 --- a/packages/kit/src/runtime/app/stores.js +++ b/packages/kit/src/runtime/app/stores.js @@ -5,6 +5,8 @@ import { stores as browser_stores } from '../client/client.js'; /** * A function that returns all of the contextual stores. On the server, this must be called during component initialization. * Only use this if you need to defer store subscription until after the component has mounted, for some reason. + * + * @deprecated Use `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) */ export const getStores = () => { const stores = BROWSER ? browser_stores : getContext('__svelte__'); @@ -28,6 +30,7 @@ export const getStores = () => { * * On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time. * + * @deprecated Use `page` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * @type {import('svelte/store').Readable} */ export const page = { @@ -43,6 +46,8 @@ export const page = { * When navigating finishes, its value reverts to `null`. * * On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time. + * + * @deprecated Use `navigating` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * @type {import('svelte/store').Readable} */ export const navigating = { @@ -56,6 +61,8 @@ export const navigating = { * A readable store whose initial value is `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. * * On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time. + * + * @deprecated Use `updated` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * @type {import('svelte/store').Readable & { check(): Promise }} */ export const updated = { diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 0c514f91e3bb..d03baae091b0 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -45,6 +45,7 @@ import { HttpError, Redirect, SvelteKitError } from '../control.js'; import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js'; import { get_message, get_status } from '../../utils/error.js'; import { writable } from 'svelte/store'; +import { page, update, navigating } from './state.svelte.js'; const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']); @@ -212,7 +213,7 @@ let hydrated = false; let started = false; let autoscroll = true; let updating = false; -let navigating = false; +let is_navigating = false; let hash_navigating = false; /** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */ let has_navigated = false; @@ -228,9 +229,6 @@ let current_history_index; /** @type {number} */ let current_navigation_index; -/** @type {import('@sveltejs/kit').Page} */ -let page; - /** @type {{}} */ let token; @@ -341,11 +339,12 @@ async function _invalidate() { } if (navigation_result.props.page) { - page = navigation_result.props.page; + Object.assign(page, navigation_result.props.page); } current = navigation_result.state; reset_invalidation(); root.$set(navigation_result.props); + update(navigation_result.props.page); } function reset_invalidation() { @@ -447,7 +446,7 @@ function initialize(result, target, hydrate) { const style = document.querySelector('style[data-sveltekit]'); if (style) style.remove(); - page = /** @type {import('@sveltejs/kit').Page} */ (result.props.page); + Object.assign(page, /** @type {import('@sveltejs/kit').Page} */ (result.props.page)); root = new app.root({ target, @@ -1240,7 +1239,7 @@ function _before_navigate({ url, type, intent, delta }) { } }; - if (!navigating) { + if (!is_navigating) { // Don't run the event during redirects before_navigate_callbacks.forEach((fn) => fn(cancellable)); } @@ -1294,10 +1293,10 @@ async function navigate({ accept(); - navigating = true; + is_navigating = true; if (started) { - stores.navigating.set(nav.navigation); + stores.navigating.set((navigating.current = nav.navigation)); } token = nav_token; @@ -1423,6 +1422,7 @@ async function navigate({ } root.$set(navigation_result.props); + update(navigation_result.props.page); has_navigated = true; } else { initialize(navigation_result, target, false); @@ -1464,10 +1464,10 @@ async function navigate({ autoscroll = true; if (navigation_result.props.page) { - page = navigation_result.props.page; + Object.assign(page, navigation_result.props.page); } - navigating = false; + is_navigating = false; if (type === 'popstate') { restore_snapshot(current_navigation_index); @@ -1479,7 +1479,7 @@ async function navigate({ fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation)) ); - stores.navigating.set(null); + stores.navigating.set((navigating.current = null)); updating = false; } @@ -1722,7 +1722,9 @@ export function disableScrollHandling() { } /** + * Allows you to navigate programmatically to a given route, with options such as keeping the current element focused. * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`. + * * For external URLs, use `window.location = url` instead of calling `goto(url)`. * * @param {string | URL} url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://svelte.dev/docs/kit/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. @@ -1731,7 +1733,7 @@ export function disableScrollHandling() { * @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation * @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body * @param {boolean} [opts.invalidateAll] If `true`, all `load` functions of the page will be rerun. See https://svelte.dev/docs/kit/load#rerunning-load-functions for more info on invalidation. - * @param {App.PageState} [opts.state] An optional object that will be available on the `$page.state` store + * @param {App.PageState} [opts.state] An optional object that will be available as `page.state` * @returns {Promise} */ export function goto(url, opts = {}) { @@ -1869,7 +1871,7 @@ export function preloadCode(pathname) { } /** - * Programmatically create a new history entry with the given `$page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing). + * Programmatically create a new history entry with the given `page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing). * * @param {string | URL} url * @param {App.PageState} state @@ -1906,14 +1908,14 @@ export function pushState(url, state) { history.pushState(opts, '', resolve_url(url)); has_navigated = true; - page = { ...page, state }; + page.state = state; root.$set({ page }); clear_onward_history(current_history_index, current_navigation_index); } /** - * Programmatically replace the current history entry with the given `$page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing). + * Programmatically replace the current history entry with the given `page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing). * * @param {string | URL} url * @param {App.PageState} state @@ -1947,12 +1949,12 @@ export function replaceState(url, state) { history.replaceState(opts, '', resolve_url(url)); - page = { ...page, state }; + page.state = state; root.$set({ page }); } /** - * This action updates the `form` property of the current page with the given data and updates `$page.status`. + * This action updates the `form` property of the current page with the given data and updates `page.status`. * In case of an error, it redirects to the nearest error page. * @template {Record | undefined} Success * @template {Record | undefined} Failure @@ -1984,18 +1986,22 @@ export async function applyAction(result) { current = navigation_result.state; root.$set(navigation_result.props); + update(navigation_result.props.page); tick().then(reset_focus); } } else if (result.type === 'redirect') { _goto(result.location, { invalidateAll: true }, 0); } else { + page.form = result.data; + page.status = result.status; + /** @type {Record} */ root.$set({ // this brings Svelte's view of the world in line with SvelteKit's // after use:enhance reset the form.... form: null, - page: { ...page, form: result.data, status: result.status } + page }); // ...so that setting the `form` prop takes effect and isn't ignored @@ -2020,7 +2026,7 @@ function _start_router() { persist_state(); - if (!navigating) { + if (!is_navigating) { const nav = create_navigation(current, undefined, null, 'leave'); // If we're navigating, beforeNavigate was already called. If we end up in here during navigation, @@ -2105,7 +2111,7 @@ function _start_router() { if (_before_navigate({ url, type: 'link' })) { // set `navigating` to `true` to prevent `beforeNavigate` callbacks // being called when the page unloads - navigating = true; + is_navigating = true; } else { event.preventDefault(); } @@ -2253,7 +2259,7 @@ function _start_router() { if (scroll) scrollTo(scroll.x, scroll.y); if (state !== page.state) { - page = { ...page, state }; + page.state = state; root.$set({ page }); } @@ -2323,7 +2329,7 @@ function _start_router() { // the navigation away from it was successful. // Info about bfcache here: https://web.dev/bfcache if (event.persisted) { - stores.navigating.set(null); + stores.navigating.set((navigating.current = null)); } }); @@ -2331,8 +2337,17 @@ function _start_router() { * @param {URL} url */ function update_url(url) { - current.url = url; - stores.page.set({ ...page, url }); + current.url = page.url = url; + stores.page.set({ + data: page.data, + error: page.error, + form: page.form, + params: page.params, + route: page.route, + state: page.state, + status: page.status, + url + }); stores.page.notify(); } } diff --git a/packages/kit/src/runtime/client/state.svelte.js b/packages/kit/src/runtime/client/state.svelte.js new file mode 100644 index 000000000000..5e1978869ca3 --- /dev/null +++ b/packages/kit/src/runtime/client/state.svelte.js @@ -0,0 +1,57 @@ +import { onMount } from 'svelte'; +import { updated_listener } from './utils.js'; + +/** @type {import('@sveltejs/kit').Page} */ +export let page; + +/** @type {{ current: import('@sveltejs/kit').Navigation | null }} */ +export let navigating; + +/** @type {{ current: boolean }} */ +export let updated; + +// this is a bootleg way to tell if we're in old svelte or new svelte +const is_legacy = + onMount.toString().includes('$$') || /function \w+\(\) \{\}/.test(onMount.toString()); + +if (is_legacy) { + page = { + data: {}, + form: null, + error: null, + params: {}, + route: { id: null }, + state: {}, + status: -1, + url: new URL('https://example.com') + }; + navigating = { current: null }; + updated = { current: false }; +} else { + page = new (class Page { + data = $state.raw({}); + form = $state.raw(null); + error = $state.raw(null); + params = $state.raw({}); + route = $state.raw({ id: null }); + state = $state.raw({}); + status = $state.raw(-1); + url = $state.raw(new URL('https://example.com')); + })(); + + navigating = new (class Navigating { + current = $state.raw(null); + })(); + + updated = new (class Updated { + current = $state.raw(false); + })(); + updated_listener.v = () => (updated.current = true); +} + +/** + * @param {import('@sveltejs/kit').Page} new_page + */ +export function update(new_page) { + Object.assign(page, new_page); +} diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index 16a1b3426a9e..51152d11b59c 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -234,6 +234,10 @@ export function notifiable_store(value) { return { notify, set, subscribe }; } +export const updated_listener = { + v: () => {} +}; + export function create_updated_store() { const { set, subscribe } = writable(false); @@ -273,6 +277,7 @@ export function create_updated_store() { if (updated) { set(true); + updated_listener.v(); clearTimeout(timeout); } diff --git a/packages/kit/src/runtime/components/svelte-5/error.svelte b/packages/kit/src/runtime/components/svelte-5/error.svelte index b82ddfaed4b4..fa83c7dcd7d4 100644 --- a/packages/kit/src/runtime/components/svelte-5/error.svelte +++ b/packages/kit/src/runtime/components/svelte-5/error.svelte @@ -1,6 +1,6 @@ -

{$page.status}

-

{$page.error?.message}

+

{page.status}

+

{page.error?.message}

diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 82c43a7ce78a..a41aa93b2894 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -110,7 +110,7 @@ export async function render_page(event, page, options, manifest, state, resolve } else if (action_result.data) { /// case: lost data console.warn( - "The form action returned a value, but it isn't available in `$page.form`, because SSR is off. To handle the returned value in CSR, enhance your form with `use:enhance`. See https://svelte.dev/docs/kit/form-actions#progressive-enhancement-use-enhance" + "The form action returned a value, but it isn't available in `page.form`, because SSR is off. To handle the returned value in CSR, enhance your form with `use:enhance`. See https://svelte.dev/docs/kit/form-actions#progressive-enhancement-use-enhance" ); } } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index b61935a581de..a3e9cddd8866 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -149,6 +149,17 @@ export async function render_response({ // portable as possible, but reset afterwards if (paths.relative) paths.override({ base, assets }); + const render_opts = { + context: new Map([ + [ + '__request__', + { + page: props.page + } + ] + ]) + }; + if (__SVELTEKIT_DEV__) { const fetch = globalThis.fetch; let warned = false; @@ -168,14 +179,14 @@ export async function render_response({ }; try { - rendered = options.root.render(props); + rendered = options.root.render(props, render_opts); } finally { globalThis.fetch = fetch; paths.reset(); } } else { try { - rendered = options.root.render(props); + rendered = options.root.render(props, render_opts); } finally { paths.reset(); } diff --git a/packages/kit/src/types/ambient.d.ts b/packages/kit/src/types/ambient.d.ts index 496fc537cf4d..5ab0c5b80c20 100644 --- a/packages/kit/src/types/ambient.d.ts +++ b/packages/kit/src/types/ambient.d.ts @@ -34,14 +34,14 @@ declare namespace App { export interface Locals {} /** - * Defines the common shape of the [$page.data store](https://svelte.dev/docs/kit/$app-stores#page) - that is, the data that is shared between all pages. + * Defines the common shape of the [page.data state](https://svelte.dev/docs/kit/$app-state#page) and [$page.data store](https://svelte.dev/docs/kit/$app-stores#page) - that is, the data that is shared between all pages. * The `Load` and `ServerLoad` functions in `./$types` will be narrowed accordingly. * Use optional properties for data that is only present on specific pages. Do not add an index signature (`[key: string]: any`). */ export interface PageData {} /** - * The shape of the `$page.state` object, which can be manipulated using the [`pushState`](https://svelte.dev/docs/kit/$app-navigation#pushState) and [`replaceState`](https://svelte.dev/docs/kit/$app-navigation#replaceState) functions from `$app/navigation`. + * The shape of the `page.state` object, which can be manipulated using the [`pushState`](https://svelte.dev/docs/kit/$app-navigation#pushState) and [`replaceState`](https://svelte.dev/docs/kit/$app-navigation#replaceState) functions from `$app/navigation`. */ export interface PageState {} diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 0c6679aab159..d838add28a54 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -306,7 +306,10 @@ export interface ServerMetadata { export interface SSRComponent { default: { - render(props: Record): { + render( + props: Record, + opts: { context: Map } + ): { html: string; head: string; css: { diff --git a/packages/kit/src/utils/url.js b/packages/kit/src/utils/url.js index 5036fa045082..96b2710cc194 100644 --- a/packages/kit/src/utils/url.js +++ b/packages/kit/src/utils/url.js @@ -168,7 +168,7 @@ function disable_hash(url) { Object.defineProperty(url, 'hash', { get() { throw new Error( - 'Cannot access event.url.hash. Consider using `$page.url.hash` inside a component instead' + 'Cannot access event.url.hash. Consider using `page.url.hash` inside a component instead' ); } }); diff --git a/packages/kit/src/utils/url.spec.js b/packages/kit/src/utils/url.spec.js index 181ffed4fd9e..e526c3ab90f2 100644 --- a/packages/kit/src/utils/url.spec.js +++ b/packages/kit/src/utils/url.spec.js @@ -125,7 +125,7 @@ describe('make_trackable', (test) => { assert.throws( () => url.hash, - /Cannot access event.url.hash. Consider using `\$page.url.hash` inside a component instead/ + /Cannot access event.url.hash. Consider using `page.url.hash` inside a component instead/ ); }); diff --git a/packages/kit/test/apps/amp/src/routes/origin/+page.svelte b/packages/kit/test/apps/amp/src/routes/origin/+page.svelte index 21aca43ddc99..ad574575c080 100644 --- a/packages/kit/test/apps/amp/src/routes/origin/+page.svelte +++ b/packages/kit/test/apps/amp/src/routes/origin/+page.svelte @@ -1,10 +1,10 @@

{data.origin}

-

{$page.url.origin}

+

{page.url.origin}

{data.data.origin}

diff --git a/packages/kit/test/apps/basics/src/routes/+error.svelte b/packages/kit/test/apps/basics/src/routes/+error.svelte index 476d730cd604..61ad908c0b25 100644 --- a/packages/kit/test/apps/basics/src/routes/+error.svelte +++ b/packages/kit/test/apps/basics/src/routes/+error.svelte @@ -1,14 +1,14 @@ - Custom error page: {$page.error.message} + Custom error page: {page.error.message} -

{$page.status}

+

{page.status}

-

This is your custom error page saying: "{$page.error.message}"

+

This is your custom error page saying: "{page.error.message}"