From 4353400591a41d735450d8afa43104b9db27833a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 16 Jan 2023 11:39:51 +0100 Subject: [PATCH 01/21] docs: state management - docs about state management - more helpful error message when trying to run a SvelteKit store on the server closes #8524 --- .../20-core-concepts/60-state-management.md | 46 +++++++++++++++++++ packages/kit/src/runtime/app/stores.js | 22 +++++++-- packages/kit/types/ambient.d.ts | 8 +++- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 documentation/docs/20-core-concepts/60-state-management.md diff --git a/documentation/docs/20-core-concepts/60-state-management.md b/documentation/docs/20-core-concepts/60-state-management.md new file mode 100644 index 000000000000..2a82c57c93a2 --- /dev/null +++ b/documentation/docs/20-core-concepts/60-state-management.md @@ -0,0 +1,46 @@ +--- +title: State Management +--- + +Managing state is one of the hardest parts in application development. In this section, we will go over the different types of state, how SvelteKit can help you with managing it, and different patterns for various use cases. + +## Types of state + +Apps have a multitude of different states. State can broadly be classified by two adjacent categories: The semantic category of the state, and how it is represented. + +Semantic categories of state are: +- Local state: State that is only relevant inside one component, and managed inside it. +- Global state: State that is relevant to many or all parts of the application, and is likely modified from different places. + +How state can be represented: +- JavaScript variable: Not visible from the outside +- URL: What page you're on, but also information for the page itself through query parameters for example +- Cookies: Mostly modifiable through the server, contains for example information about the current user. Can be used to persist state across browser tabs or reloads. +- localStorage/sessionStorage: Persist state across tabs or reloads, for example user settings. +- API/Backend: Data that is saved inside a database, for example the list of TODOs the user entered. + +The above are not exhaustive lists, and different measures could be used to categorize state. The key point is to think about state systematically before adding it to your application. + +## Careful with global state + +If you are creating an [SPA](/docs/glossary#csr-and-spa) with SvelteKit, you can create global state freely, as you can be sure that it's only initialized inside the user's browser. If you use [SSR](/docs/glossary#ssr) however, you have to watch out for a couple of things when managing state. In many server environments, a single instance of your app will serve multiple users (this is not specific to SvelteKit - it's one of the gotchas of working with such environments). For that reason, per-request or per-user state must not be stored in global variables. + +Consider the following example where the user is set from inside a `load` function: + +```js +/// file: +page.js +// DON'T DO THIS! +import { user } from '$lib/user'; + +/** @type {import('./$types').PageLoad} */ +export async function load({ fetch }) { + const response = await fetch('/api/user'); + user.set(await response.json()); +} +``` + +If you are using SSR, the `load` function will run on the server initially, which means that the whole server instance which serves _all_ requests from _all_ users has its `user` state set to the one just requested from the API. To scope this to a single user, you have a couple of options: + +- if you need to access the user state only inside `load` functions, use `locals` in server `load` functions +- if you need to persist the user state across reloads, but only need to access it inside `load` functions, use `cookies` in server `load` functions +- if you need to access and update the state inside components, use Svelte's [context feature](https://svelte.dev/docs#run-time-svelte-setcontext). That way, the state is scoped to components, which means they are not shared across different requests on the server. The drawback is that you can only set and subscribe to the store at component initialization. SvelteKit's stores from `$app/stores` for example are setup like this (which is why you may have encountered a related error message) diff --git a/packages/kit/src/runtime/app/stores.js b/packages/kit/src/runtime/app/stores.js index 5c79a7ad3f94..671f972660c8 100644 --- a/packages/kit/src/runtime/app/stores.js +++ b/packages/kit/src/runtime/app/stores.js @@ -23,7 +23,7 @@ export const getStores = () => { export const page = { /** @param {(value: any) => void} fn */ subscribe(fn) { - const store = getStores().page; + const store = __SVELTEKIT_DEV__ ? get_store('page') : getStores().page; return store.subscribe(fn); } }; @@ -31,7 +31,7 @@ export const page = { /** @type {typeof import('$app/stores').navigating} */ export const navigating = { subscribe(fn) { - const store = getStores().navigating; + const store = __SVELTEKIT_DEV__ ? get_store('navigating') : getStores().navigating; return store.subscribe(fn); } }; @@ -39,7 +39,7 @@ export const navigating = { /** @type {typeof import('$app/stores').updated} */ export const updated = { subscribe(fn) { - const store = getStores().updated; + const store = __SVELTEKIT_DEV__ ? get_store('updated') : getStores().updated; if (browser) { updated.check = store.check; @@ -55,3 +55,19 @@ export const updated = { ); } }; + +/** + * @template {keyof ReturnType} Name + * @param {Name} name + * @returns {ReturnType[Name]} + */ +function get_store(name) { + try { + return getStores()[name]; + } catch (e) { + throw new Error( + `Store '${name}' is not available outside of a Svelte component on the server, as its bound to component context there. ` + + 'For more information, see https://kit.svelte.dev/docs/state-management#careful-with-global-state' + ); + } +} diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts index f781c274dcaa..237521285543 100644 --- a/packages/kit/types/ambient.d.ts +++ b/packages/kit/types/ambient.d.ts @@ -287,16 +287,22 @@ declare module '$app/stores' { /** * A readable store whose value contains page data. + * + * On the server, this store can only be subscribed to during component initialization. On the client, it can be subscribed to at any time. */ export const page: Readable; /** * A readable store. * When navigating starts, its value is a `Navigation` object with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. * When navigating finishes, its value reverts to `null`. + * + * On the server, this store can only be subscribed to during component initialization. On the client, it can be subscribed to at any time. */ export const navigating: Readable; /** - * A readable store whose initial value is `false`. If [`version.pollInterval`](https://kit.svelte.dev/docs/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. + * A readable store whose initial value is `false`. If [`version.pollInterval`](https://kit.svelte.dev/docs/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. On the client, it can be subscribed to at any time. */ export const updated: Readable & { check(): Promise }; From b198f5919b57b9342e534c8e6414b12e3ab26a44 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 16 Jan 2023 11:44:03 +0100 Subject: [PATCH 02/21] fix --- .../docs/20-core-concepts/60-state-management.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/documentation/docs/20-core-concepts/60-state-management.md b/documentation/docs/20-core-concepts/60-state-management.md index 2a82c57c93a2..e2e28fa10950 100644 --- a/documentation/docs/20-core-concepts/60-state-management.md +++ b/documentation/docs/20-core-concepts/60-state-management.md @@ -27,8 +27,15 @@ If you are creating an [SPA](/docs/glossary#csr-and-spa) with SvelteKit, you can Consider the following example where the user is set from inside a `load` function: -```js +```js /// file: +page.js +// @filename: ambient.d.ts +declare module '$lib/user' { + export const user = { set: (value: any) => void }; +} + +// @filename: index.js +// ---cut--- // DON'T DO THIS! import { user } from '$lib/user'; From c1ffd0b24f136450a3068dba0717cd04d3b88323 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 16 Jan 2023 12:01:37 +0100 Subject: [PATCH 03/21] fix the fix --- documentation/docs/20-core-concepts/60-state-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/20-core-concepts/60-state-management.md b/documentation/docs/20-core-concepts/60-state-management.md index e2e28fa10950..cb1477e41b08 100644 --- a/documentation/docs/20-core-concepts/60-state-management.md +++ b/documentation/docs/20-core-concepts/60-state-management.md @@ -31,7 +31,7 @@ Consider the following example where the user is set from inside a `load` functi /// file: +page.js // @filename: ambient.d.ts declare module '$lib/user' { - export const user = { set: (value: any) => void }; + export const user: { set: (value: any) => void }; } // @filename: index.js From dba26f3443d41661c8b5780e9a8fbe996d765eff Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 24 Jan 2023 17:52:31 +0100 Subject: [PATCH 04/21] update --- .../20-core-concepts/60-state-management.md | 81 ++++++++++++++----- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/documentation/docs/20-core-concepts/60-state-management.md b/documentation/docs/20-core-concepts/60-state-management.md index cb1477e41b08..bfc5d5598bfa 100644 --- a/documentation/docs/20-core-concepts/60-state-management.md +++ b/documentation/docs/20-core-concepts/60-state-management.md @@ -2,24 +2,7 @@ title: State Management --- -Managing state is one of the hardest parts in application development. In this section, we will go over the different types of state, how SvelteKit can help you with managing it, and different patterns for various use cases. - -## Types of state - -Apps have a multitude of different states. State can broadly be classified by two adjacent categories: The semantic category of the state, and how it is represented. - -Semantic categories of state are: -- Local state: State that is only relevant inside one component, and managed inside it. -- Global state: State that is relevant to many or all parts of the application, and is likely modified from different places. - -How state can be represented: -- JavaScript variable: Not visible from the outside -- URL: What page you're on, but also information for the page itself through query parameters for example -- Cookies: Mostly modifiable through the server, contains for example information about the current user. Can be used to persist state across browser tabs or reloads. -- localStorage/sessionStorage: Persist state across tabs or reloads, for example user settings. -- API/Backend: Data that is saved inside a database, for example the list of TODOs the user entered. - -The above are not exhaustive lists, and different measures could be used to categorize state. The key point is to think about state systematically before adding it to your application. +Managing state is one of the hardest parts in application development. This section covers various use cases with regards to state management and what to watch out for. ## Careful with global state @@ -41,8 +24,8 @@ import { user } from '$lib/user'; /** @type {import('./$types').PageLoad} */ export async function load({ fetch }) { - const response = await fetch('/api/user'); - user.set(await response.json()); + const response = await fetch('/api/user'); + user.set(await response.json()); } ``` @@ -51,3 +34,61 @@ If you are using SSR, the `load` function will run on the server initially, whic - if you need to access the user state only inside `load` functions, use `locals` in server `load` functions - if you need to persist the user state across reloads, but only need to access it inside `load` functions, use `cookies` in server `load` functions - if you need to access and update the state inside components, use Svelte's [context feature](https://svelte.dev/docs#run-time-svelte-setcontext). That way, the state is scoped to components, which means they are not shared across different requests on the server. The drawback is that you can only set and subscribe to the store at component initialization. SvelteKit's stores from `$app/stores` for example are setup like this (which is why you may have encountered a related error message) + +If you have global data whose initial state is not dependent on a request (in other words, it's always the same), then you can keep storing that data globally, as long as you make sure you don't update it during the initial rendering on the server (during load or component render). + +## Managing forms + +If you are coming from regular Svelte, you may be used to do something like this when creating a form to react to user input: + +```svelte +/// file: send-message/+page.svelte + + +
+ +
+``` + +```js +/// file: api/message/+server.js + +/** @type {import('./$types').RequestHandler} */ +export function POST() { + // store data somewhere +} +``` + +This works well when JavaScript is available but results in unresponsive UI when it isn't (which may be [more often than you think](https://kryogenix.org/code/browser/everyonehasjs.html)). If this is a concern to you, leverage SvelteKit's [form actions](/docs/form-actions) instead. Apart from being able to build more resilient apps, this may also improve your code base because you for example can colocate the backend logic of posting the message next to the page triggering it. The same code using form actions would look roughly like this: + +```svelte +/// file: send-message/+page.svelte + + +
+ +
+``` + +```js +/// file: send-message/+page.server.js + +/** @type {import('./$types').Actions} */ +export const actions = { + default: (request) => { + // store data somewhere + } +} +``` From 72015096ca24aadf2397711588b2235383d8217d Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 24 Jan 2023 17:55:46 +0100 Subject: [PATCH 05/21] fix link --- documentation/docs/20-core-concepts/60-state-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/20-core-concepts/60-state-management.md b/documentation/docs/20-core-concepts/60-state-management.md index bfc5d5598bfa..24af029f0cb8 100644 --- a/documentation/docs/20-core-concepts/60-state-management.md +++ b/documentation/docs/20-core-concepts/60-state-management.md @@ -6,7 +6,7 @@ Managing state is one of the hardest parts in application development. This sect ## Careful with global state -If you are creating an [SPA](/docs/glossary#csr-and-spa) with SvelteKit, you can create global state freely, as you can be sure that it's only initialized inside the user's browser. If you use [SSR](/docs/glossary#ssr) however, you have to watch out for a couple of things when managing state. In many server environments, a single instance of your app will serve multiple users (this is not specific to SvelteKit - it's one of the gotchas of working with such environments). For that reason, per-request or per-user state must not be stored in global variables. +If you are creating an [SPA](/docs/glossary#csr) with SvelteKit, you can create global state freely, as you can be sure that it's only initialized inside the user's browser. If you use [SSR](/docs/glossary#ssr) however, you have to watch out for a couple of things when managing state. In many server environments, a single instance of your app will serve multiple users (this is not specific to SvelteKit - it's one of the gotchas of working with such environments). For that reason, per-request or per-user state must not be stored in global variables. Consider the following example where the user is set from inside a `load` function: From 53285d992675759dcd67ebe01517c339f2a6f90d Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 25 Jan 2023 09:56:08 +0100 Subject: [PATCH 06/21] Update documentation/docs/20-core-concepts/60-state-management.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- documentation/docs/20-core-concepts/60-state-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/20-core-concepts/60-state-management.md b/documentation/docs/20-core-concepts/60-state-management.md index 24af029f0cb8..cc6df8d38831 100644 --- a/documentation/docs/20-core-concepts/60-state-management.md +++ b/documentation/docs/20-core-concepts/60-state-management.md @@ -39,7 +39,7 @@ If you have global data whose initial state is not dependent on a request (in ot ## Managing forms -If you are coming from regular Svelte, you may be used to do something like this when creating a form to react to user input: +If you are coming from regular Svelte, you may be used to doing something like this when creating a form to react to user input: ```svelte /// file: send-message/+page.svelte From 978ac41f4ab0a7d8bd983ef98929a3fb3be5d35c Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 27 Jan 2023 22:51:29 +0100 Subject: [PATCH 07/21] Update packages/kit/src/runtime/app/stores.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomasz Olędzki --- packages/kit/src/runtime/app/stores.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/app/stores.js b/packages/kit/src/runtime/app/stores.js index 671f972660c8..f04f0f332113 100644 --- a/packages/kit/src/runtime/app/stores.js +++ b/packages/kit/src/runtime/app/stores.js @@ -66,7 +66,7 @@ function get_store(name) { return getStores()[name]; } catch (e) { throw new Error( - `Store '${name}' is not available outside of a Svelte component on the server, as its bound to component context there. ` + + `Store '${name}' is not available outside of a Svelte component on the server, as it's bound to component context there. ` + 'For more information, see https://kit.svelte.dev/docs/state-management#careful-with-global-state' ); } From fed53b2f41db345d4d03b901c527835c0cbc2ba0 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 8 Feb 2023 13:04:26 +0100 Subject: [PATCH 08/21] move details to form actions, add url section --- .../docs/20-core-concepts/30-form-actions.md | 32 ++++++- .../20-core-concepts/50-state-management.md | 46 +++++++++ .../20-core-concepts/60-state-management.md | 94 ------------------- 3 files changed, 77 insertions(+), 95 deletions(-) create mode 100644 documentation/docs/20-core-concepts/50-state-management.md delete mode 100644 documentation/docs/20-core-concepts/60-state-management.md diff --git a/documentation/docs/20-core-concepts/30-form-actions.md b/documentation/docs/20-core-concepts/30-form-actions.md index bfd57beab78d..baa403cd0f9d 100644 --- a/documentation/docs/20-core-concepts/30-form-actions.md +++ b/documentation/docs/20-core-concepts/30-form-actions.md @@ -456,7 +456,37 @@ const response = await fetch(this.action, { ## Alternatives -Form actions are the preferred way to send data to the server, since they can be progressively enhanced, but you can also use [`+server.js`](routing#server) files to expose (for example) a JSON API. +Form actions are the preferred way to send data to the server, since they can be progressively enhanced, but you can also use [`+server.js`](routing#server) files to expose (for example) a JSON API. If you are coming from regular Svelte, you may be used to doing something like this when creating a form to react to user input: + +```svelte +/// file: send-message/+page.svelte + + +
+ +
+``` + +```js +/// file: api/message/+server.js + +/** @type {import('./$types').RequestHandler} */ +export function POST() { + // store data somewhere +} +``` + +Try to use form actions where possible instead. ## GET vs POST diff --git a/documentation/docs/20-core-concepts/50-state-management.md b/documentation/docs/20-core-concepts/50-state-management.md new file mode 100644 index 000000000000..e3b3c9d85f0f --- /dev/null +++ b/documentation/docs/20-core-concepts/50-state-management.md @@ -0,0 +1,46 @@ +--- +title: State Management +--- + +Managing state is one of the hardest parts in application development. This section covers various use cases with regards to state management and what to watch out for. + +## Avoid global state in SSR + +If you are creating an [SPA](glossary#csr) with SvelteKit, you can create global state freely, as you can be sure that it's only initialized inside the user's browser. If you use [SSR](glossary#ssr) however, you have to watch out for a couple of things when managing state. In many server environments, a single instance of your app will serve multiple users (this is not specific to SvelteKit - it's one of the gotchas of working with such environments). For that reason, per-request or per-user state must not be stored in global variables. + +Consider the following example where the user is set from inside a `load` function: + +```js +/// file: +page.js +// @filename: ambient.d.ts +declare module '$lib/user' { + export const user: { set: (value: any) => void }; +} + +// @filename: index.js +// ---cut--- +// DON'T DO THIS! +import { user } from '$lib/user'; + +/** @type {import('./$types').PageLoad} */ +export async function load({ fetch }) { + const response = await fetch('/api/user'); + user.set(await response.json()); +} +``` + +If you are using SSR, the `load` function will run on the server initially, which means that the whole server instance which serves _all_ requests from _all_ users has its `user` state set to the one just requested from the API. To scope this to a single user, you have a couple of options: + +- if you need to access the user state only inside server `load` functions, use `locals` +- if you need to persist the user state across reloads, but only need to access it inside `load` functions, use `cookies` in server `load` functions +- if you need to access and update the state inside components, use Svelte's [context feature](https://svelte.dev/docs#run-time-svelte-setcontext). That way, the state is scoped to components, which means they are not shared across different requests on the server. The drawback is that you can only set and subscribe to the store at component initialization. SvelteKit's stores from `$app/stores` for example are setup like this (which is why you may have encountered a related error message) + +If you have global data whose initial state is not dependent on a request (in other words, it's always the same), then you can keep storing that data globally, as long as you make sure you don't update it during the initial rendering on the server (during load or component render). + +## Managing forms + +When coming from a pure Svelte or JavaScript background, you might be used to handling all form interactions through JavaScript. This works well when JavaScript is available but results in unresponsive UI when it isn't (which may be [more often than you think](https://kryogenix.org/code/browser/everyonehasjs.html)). If this is a concern to you, leverage SvelteKit's [form actions](form-actions) instead. + +## Leverage the URL as state + +UI-only state like "is the accordion open" are ok to store as component-level state that does not survive page reloads. Other state such as selected filters on a shopping page are better stored inside the URL as query parameters. That way they survive reloads and are accessible inside `load` functions through the `url` property. diff --git a/documentation/docs/20-core-concepts/60-state-management.md b/documentation/docs/20-core-concepts/60-state-management.md deleted file mode 100644 index cc6df8d38831..000000000000 --- a/documentation/docs/20-core-concepts/60-state-management.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: State Management ---- - -Managing state is one of the hardest parts in application development. This section covers various use cases with regards to state management and what to watch out for. - -## Careful with global state - -If you are creating an [SPA](/docs/glossary#csr) with SvelteKit, you can create global state freely, as you can be sure that it's only initialized inside the user's browser. If you use [SSR](/docs/glossary#ssr) however, you have to watch out for a couple of things when managing state. In many server environments, a single instance of your app will serve multiple users (this is not specific to SvelteKit - it's one of the gotchas of working with such environments). For that reason, per-request or per-user state must not be stored in global variables. - -Consider the following example where the user is set from inside a `load` function: - -```js -/// file: +page.js -// @filename: ambient.d.ts -declare module '$lib/user' { - export const user: { set: (value: any) => void }; -} - -// @filename: index.js -// ---cut--- -// DON'T DO THIS! -import { user } from '$lib/user'; - -/** @type {import('./$types').PageLoad} */ -export async function load({ fetch }) { - const response = await fetch('/api/user'); - user.set(await response.json()); -} -``` - -If you are using SSR, the `load` function will run on the server initially, which means that the whole server instance which serves _all_ requests from _all_ users has its `user` state set to the one just requested from the API. To scope this to a single user, you have a couple of options: - -- if you need to access the user state only inside `load` functions, use `locals` in server `load` functions -- if you need to persist the user state across reloads, but only need to access it inside `load` functions, use `cookies` in server `load` functions -- if you need to access and update the state inside components, use Svelte's [context feature](https://svelte.dev/docs#run-time-svelte-setcontext). That way, the state is scoped to components, which means they are not shared across different requests on the server. The drawback is that you can only set and subscribe to the store at component initialization. SvelteKit's stores from `$app/stores` for example are setup like this (which is why you may have encountered a related error message) - -If you have global data whose initial state is not dependent on a request (in other words, it's always the same), then you can keep storing that data globally, as long as you make sure you don't update it during the initial rendering on the server (during load or component render). - -## Managing forms - -If you are coming from regular Svelte, you may be used to doing something like this when creating a form to react to user input: - -```svelte -/// file: send-message/+page.svelte - - -
- -
-``` - -```js -/// file: api/message/+server.js - -/** @type {import('./$types').RequestHandler} */ -export function POST() { - // store data somewhere -} -``` - -This works well when JavaScript is available but results in unresponsive UI when it isn't (which may be [more often than you think](https://kryogenix.org/code/browser/everyonehasjs.html)). If this is a concern to you, leverage SvelteKit's [form actions](/docs/form-actions) instead. Apart from being able to build more resilient apps, this may also improve your code base because you for example can colocate the backend logic of posting the message next to the page triggering it. The same code using form actions would look roughly like this: - -```svelte -/// file: send-message/+page.svelte - - -
- -
-``` - -```js -/// file: send-message/+page.server.js - -/** @type {import('./$types').Actions} */ -export const actions = { - default: (request) => { - // store data somewhere - } -} -``` From 044e3cbb89277e77b3e980e3e7194ea2e6e7dc0f Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 8 Feb 2023 14:12:15 +0100 Subject: [PATCH 09/21] shorten, do not use form in example --- .../docs/20-core-concepts/30-form-actions.md | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/documentation/docs/20-core-concepts/30-form-actions.md b/documentation/docs/20-core-concepts/30-form-actions.md index baa403cd0f9d..99e774912147 100644 --- a/documentation/docs/20-core-concepts/30-form-actions.md +++ b/documentation/docs/20-core-concepts/30-form-actions.md @@ -456,38 +456,30 @@ const response = await fetch(this.action, { ## Alternatives -Form actions are the preferred way to send data to the server, since they can be progressively enhanced, but you can also use [`+server.js`](routing#server) files to expose (for example) a JSON API. If you are coming from regular Svelte, you may be used to doing something like this when creating a form to react to user input: +Form actions are the preferred way to send data to the server, since they can be progressively enhanced, but you can also use [`+server.js`](routing#server) files to expose (for example) a JSON API. Here's how such an interaction could look like: ```svelte /// file: send-message/+page.svelte -
- -
+ ``` ```js -/// file: api/message/+server.js +/// file: api/ci/+server.js /** @type {import('./$types').RequestHandler} */ export function POST() { - // store data somewhere + // do something } ``` -Try to use form actions where possible instead. - ## GET vs POST As we've seen, to invoke a form action you must use `method="POST"`. From 98c9b13b25d1c62119f3a5d8aa3d922b4685c886 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 8 Feb 2023 14:25:33 +0100 Subject: [PATCH 10/21] remove now obsolete paragraph, add info on when params and url can change --- documentation/docs/20-core-concepts/20-load.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index 4a563afc9002..cab3da86cf79 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -454,7 +454,7 @@ export function load() { When rendering (or navigating to) a page, SvelteKit runs all `load` functions concurrently, avoiding a waterfall of requests. During client-side navigation, the result of calling multiple server `load` functions are grouped into a single response. Once all `load` functions have returned, the page is rendered. -## Invalidation +## Rerunning load functions SvelteKit tracks the dependencies of each `load` function to avoid re-running it unnecessarily during navigation. @@ -553,8 +553,6 @@ To summarize, a `load` function will re-run in the following situations: - It declared a dependency on a specific URL via [`fetch`](#making-fetch-requests) or [`depends`](types#public-types-loadevent), and that URL was marked invalid with [`invalidate(url)`](modules#$app-navigation-invalidate) - All active `load` functions were forcibly re-run with [`invalidateAll()`](modules#$app-navigation-invalidateall) -Note that re-running a `load` function will update the `data` prop inside the corresponding `+layout.svelte` or `+page.svelte`; it does _not_ cause the component to be recreated. As a result, internal state is preserved. If this isn't what you want, you can reset whatever you need to reset inside an [`afterNavigate`](modules#$app-navigation-afternavigate) callback, and/or wrap your component in a [`{#key ...}`](https://svelte.dev/docs#template-syntax-key) block. - -## Shared state +`params` and `url` can change in response to a `` link click, a [`
` interaction](form-actions#get-vs-post), a [`goto`](modules#$app-navigation-goto) invocation, or a [`redirect`](modules#sveltejs-kit-redirect). -In many server environments, a single instance of your app will serve multiple users. For that reason, per-request or per-user state must not be stored in shared variables outside your `load` functions, but should instead be stored in `event.locals`. +Note that re-running a `load` function will update the `data` prop inside the corresponding `+layout.svelte` or `+page.svelte`; it does _not_ cause the component to be recreated. As a result, internal state is preserved. If this isn't what you want, you can reset whatever you need to reset inside an [`afterNavigate`](modules#$app-navigation-afternavigate) callback, and/or wrap your component in a [`{#key ...}`](https://svelte.dev/docs#template-syntax-key) block. From bd87958000d578e410ad17726f1e2d5331c4f212 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 8 Feb 2023 14:36:02 +0100 Subject: [PATCH 11/21] links --- documentation/docs/20-core-concepts/50-state-management.md | 4 ++-- packages/kit/src/runtime/app/stores.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/docs/20-core-concepts/50-state-management.md b/documentation/docs/20-core-concepts/50-state-management.md index e3b3c9d85f0f..e35db7838857 100644 --- a/documentation/docs/20-core-concepts/50-state-management.md +++ b/documentation/docs/20-core-concepts/50-state-management.md @@ -31,8 +31,8 @@ export async function load({ fetch }) { If you are using SSR, the `load` function will run on the server initially, which means that the whole server instance which serves _all_ requests from _all_ users has its `user` state set to the one just requested from the API. To scope this to a single user, you have a couple of options: -- if you need to access the user state only inside server `load` functions, use `locals` -- if you need to persist the user state across reloads, but only need to access it inside `load` functions, use `cookies` in server `load` functions +- if you need to access the user state only inside server `load` functions, use [`locals`](hooks#server-hooks-handle) +- if you need to persist the user state across reloads, but only need to access it inside `load` functions, use [`cookies` in server `load` functions](load#cookies-and-headers) - if you need to access and update the state inside components, use Svelte's [context feature](https://svelte.dev/docs#run-time-svelte-setcontext). That way, the state is scoped to components, which means they are not shared across different requests on the server. The drawback is that you can only set and subscribe to the store at component initialization. SvelteKit's stores from `$app/stores` for example are setup like this (which is why you may have encountered a related error message) If you have global data whose initial state is not dependent on a request (in other words, it's always the same), then you can keep storing that data globally, as long as you make sure you don't update it during the initial rendering on the server (during load or component render). diff --git a/packages/kit/src/runtime/app/stores.js b/packages/kit/src/runtime/app/stores.js index f04f0f332113..fbcb94673f07 100644 --- a/packages/kit/src/runtime/app/stores.js +++ b/packages/kit/src/runtime/app/stores.js @@ -67,7 +67,7 @@ function get_store(name) { } catch (e) { throw new Error( `Store '${name}' is not available outside of a Svelte component on the server, as it's bound to component context there. ` + - 'For more information, see https://kit.svelte.dev/docs/state-management#careful-with-global-state' + 'For more information, see https://kit.svelte.dev/docs/state-management#avoid-global-state-in-ssr' ); } } From aecbc2ba4632d37cfa041c4072ff3db73e5a5f6a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 8 Feb 2023 14:39:42 +0100 Subject: [PATCH 12/21] changeset, link fix --- .changeset/large-toes-fetch.md | 5 +++++ packages/kit/types/ambient.d.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/large-toes-fetch.md diff --git a/.changeset/large-toes-fetch.md b/.changeset/large-toes-fetch.md new file mode 100644 index 000000000000..1eedcb65e0ae --- /dev/null +++ b/.changeset/large-toes-fetch.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: better error message when accessing built-in stores on the server diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts index 3e848073b30d..8833cdf8ea79 100644 --- a/packages/kit/types/ambient.d.ts +++ b/packages/kit/types/ambient.d.ts @@ -195,7 +195,7 @@ declare module '$app/navigation' { */ state?: any; /** - * If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#invalidation for more info on invalidation. + * If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#rerunning-load-functions for more info on invalidation. */ invalidateAll?: boolean; } From a18f31fa968b66b1a9635a768bdc8ea85edee060 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 8 Feb 2023 14:58:47 +0100 Subject: [PATCH 13/21] info on when which load function runs --- documentation/docs/20-core-concepts/20-load.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index cab3da86cf79..6a511abf1d9b 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -162,6 +162,14 @@ As we've seen, there are two types of `load` function: Conceptually, they're the same thing, but there are some important differences to be aware of. +### When does which load function run + +Server `load` functions _always_ run on the server. + +By default universal `load` functions run on the server during SSR when the user first visits your page. They will then run again during hydration, reusing any responses from [fetch requests](#making-fetch-requests). All subsequent invocations of universal `load` functions happen in the browser. You can customize the behavior through [page options](page-options). If you disable [server side rendering](page-options#ssr), you'll get an SPA and universal `load` functions _always_ run on the client. + +A `load` function is invoked at runtime, unless you [prerender](page-options#prerender) the page — in that case, it's invoked at build time. + ### Input Both universal and server `load` functions have access to properties describing the request (`params`, `route` and `url`) and various functions (`fetch`, `setHeaders`, `parent` and `depends`). These are described in the following sections. From 1d123cba8e62a2f45c998edcdb1adb95d8ebcb56 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 8 Feb 2023 15:20:47 +0100 Subject: [PATCH 14/21] fix --- documentation/docs/20-core-concepts/30-form-actions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/docs/20-core-concepts/30-form-actions.md b/documentation/docs/20-core-concepts/30-form-actions.md index 99e774912147..2935de4af5a1 100644 --- a/documentation/docs/20-core-concepts/30-form-actions.md +++ b/documentation/docs/20-core-concepts/30-form-actions.md @@ -472,6 +472,7 @@ Form actions are the preferred way to send data to the server, since they can be ``` ```js +// @errors: 2355 1360 /// file: api/ci/+server.js /** @type {import('./$types').RequestHandler} */ From 2e69c32ce0ca975b57cd34712f9ebed92ab2b36e Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 8 Feb 2023 16:20:06 +0100 Subject: [PATCH 15/21] closes #8302 --- documentation/docs/20-core-concepts/20-load.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index 6a511abf1d9b..d418a47f4670 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -234,7 +234,7 @@ To get data from an external API or a `+server.js` handler, you can use the prov - it can be used to make credentialed requests on the server, as it inherits the `cookie` and `authorization` headers for the page request - it can make relative requests on the server (ordinarily, `fetch` requires a URL with an origin when used in a server context) - internal requests (e.g. for `+server.js` routes) go direct to the handler function when running on the server, without the overhead of an HTTP call -- during server-side rendering, the response will be captured and inlined into the rendered HTML. Note that headers will _not_ be serialized, unless explicitly included via [`filterSerializedResponseHeaders`](hooks#server-hooks-handle). Then, during hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request - if you got a warning in your browser console when using the browser `fetch` instead of the `load` `fetch`, this is why. +- during server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the `text` and `json` methods of the `Response` object. Note that headers will _not_ be serialized, unless explicitly included via [`filterSerializedResponseHeaders`](hooks#server-hooks-handle). Then, during hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request - if you got a warning in your browser console when using the browser `fetch` instead of the `load` `fetch`, this is why. ```js /// file: src/routes/items/[id]/+page.js From e9081962e62744bc51d6e91d9da15e524e9f54d9 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 9 Feb 2023 09:52:28 +0100 Subject: [PATCH 16/21] Update documentation/docs/20-core-concepts/50-state-management.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- documentation/docs/20-core-concepts/50-state-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/20-core-concepts/50-state-management.md b/documentation/docs/20-core-concepts/50-state-management.md index e35db7838857..ee9b8a0a4b6b 100644 --- a/documentation/docs/20-core-concepts/50-state-management.md +++ b/documentation/docs/20-core-concepts/50-state-management.md @@ -6,7 +6,7 @@ Managing state is one of the hardest parts in application development. This sect ## Avoid global state in SSR -If you are creating an [SPA](glossary#csr) with SvelteKit, you can create global state freely, as you can be sure that it's only initialized inside the user's browser. If you use [SSR](glossary#ssr) however, you have to watch out for a couple of things when managing state. In many server environments, a single instance of your app will serve multiple users (this is not specific to SvelteKit - it's one of the gotchas of working with such environments). For that reason, per-request or per-user state must not be stored in global variables. +If you are creating a [single page application (SPA)](glossary#spa) with SvelteKit, you can create global state freely, as you can be sure that it's only initialized inside the user's browser. If you use [SSR](glossary#ssr) however, you have to watch out for a couple of things when managing state. In many server environments, a single instance of your app will serve multiple users (this is not specific to SvelteKit - it's one of the gotchas of working with such environments). For that reason, per-request or per-user state must not be stored in global variables. Consider the following example where the user is set from inside a `load` function: From 99a8907d0e10afc29d9a256f4ed9a183a5c4afaf Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 9 Feb 2023 10:06:11 +0100 Subject: [PATCH 17/21] details, example --- .../20-core-concepts/50-state-management.md | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/documentation/docs/20-core-concepts/50-state-management.md b/documentation/docs/20-core-concepts/50-state-management.md index e35db7838857..f694fb0a5e31 100644 --- a/documentation/docs/20-core-concepts/50-state-management.md +++ b/documentation/docs/20-core-concepts/50-state-management.md @@ -31,9 +31,38 @@ export async function load({ fetch }) { If you are using SSR, the `load` function will run on the server initially, which means that the whole server instance which serves _all_ requests from _all_ users has its `user` state set to the one just requested from the API. To scope this to a single user, you have a couple of options: -- if you need to access the user state only inside server `load` functions, use [`locals`](hooks#server-hooks-handle) -- if you need to persist the user state across reloads, but only need to access it inside `load` functions, use [`cookies` in server `load` functions](load#cookies-and-headers) -- if you need to access and update the state inside components, use Svelte's [context feature](https://svelte.dev/docs#run-time-svelte-setcontext). That way, the state is scoped to components, which means they are not shared across different requests on the server. The drawback is that you can only set and subscribe to the store at component initialization. SvelteKit's stores from `$app/stores` for example are setup like this (which is why you may have encountered a related error message) +- if you need to access the state only inside server `load` functions, use [`locals`](hooks#server-hooks-handle) +- if you need to persist the state across reloads, but only need to access it inside `load` functions, use [`cookies` in server `load` functions](load#cookies-and-headers). If the state is more complex, safe a key to the state in the cookie to look it up in for example a database +- if you need to access and update the state inside components, use Svelte's [context feature](https://svelte.dev/docs#run-time-svelte-setcontext). That way, the state is scoped to components, which means they are not shared across different requests on the server. The drawback is that you can only access the context at component initialization, which may make interacting with the store value a little trickier if you want to do that outside of components. SvelteKit's stores from `$app/stores` for example are setup like this (which is why you may have encountered a related error message) + +```svelte +/// +layout.svelte + +``` + +```svelte +/// +src/user/+page.svelte + + +

Welcome {$user.name}

+``` If you have global data whose initial state is not dependent on a request (in other words, it's always the same), then you can keep storing that data globally, as long as you make sure you don't update it during the initial rendering on the server (during load or component render). From 29a8a4cd907b21513e8c74fdd4117c6648092c7e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 16 Feb 2023 10:15:37 +0100 Subject: [PATCH 18/21] Update documentation/docs/20-core-concepts/20-load.md Co-authored-by: Harry Allen <66224939+HarryAllen1@users.noreply.github.com> --- documentation/docs/20-core-concepts/20-load.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index d418a47f4670..083173ee5c02 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -166,7 +166,7 @@ Conceptually, they're the same thing, but there are some important differences t Server `load` functions _always_ run on the server. -By default universal `load` functions run on the server during SSR when the user first visits your page. They will then run again during hydration, reusing any responses from [fetch requests](#making-fetch-requests). All subsequent invocations of universal `load` functions happen in the browser. You can customize the behavior through [page options](page-options). If you disable [server side rendering](page-options#ssr), you'll get an SPA and universal `load` functions _always_ run on the client. +By default, universal `load` functions run on the server during SSR when the user first visits your page. They will then run again during hydration, reusing any responses from [fetch requests](#making-fetch-requests). All subsequent invocations of universal `load` functions happen in the browser. You can customize the behavior through [page options](page-options). If you disable [server side rendering](page-options#ssr), you'll get an SPA and universal `load` functions _always_ run on the client. A `load` function is invoked at runtime, unless you [prerender](page-options#prerender) the page — in that case, it's invoked at build time. From 42b8db8c768051717e3acb08bb09f3cb779edcd1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Feb 2023 21:13:15 -0500 Subject: [PATCH 19/21] Update documentation/docs/20-core-concepts/20-load.md --- documentation/docs/20-core-concepts/20-load.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index df2ceb0e5ecb..f8937247e465 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -162,7 +162,7 @@ As we've seen, there are two types of `load` function: Conceptually, they're the same thing, but there are some important differences to be aware of. -### When does which load function run +### When does which load function run? Server `load` functions _always_ run on the server. From b7e90e3cc768523d000d223da02b43dff10dd7b3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Feb 2023 21:20:22 -0500 Subject: [PATCH 20/21] Update documentation/docs/20-core-concepts/50-state-management.md --- documentation/docs/20-core-concepts/50-state-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/20-core-concepts/50-state-management.md b/documentation/docs/20-core-concepts/50-state-management.md index 4e6823fb74ae..a31c39d9deb6 100644 --- a/documentation/docs/20-core-concepts/50-state-management.md +++ b/documentation/docs/20-core-concepts/50-state-management.md @@ -1,5 +1,5 @@ --- -title: State Management +title: State management --- Managing state is one of the hardest parts in application development. This section covers various use cases with regards to state management and what to watch out for. From 87c06c589fa0b6068306ad7334dfacdffab36622 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Feb 2023 12:20:04 -0500 Subject: [PATCH 21/21] docs: state management part 2 (#9239) * updates * fix broken link * fix file annotations * tweak * Update documentation/docs/20-core-concepts/50-state-management.md * Update documentation/docs/20-core-concepts/50-state-management.md Co-authored-by: Rich Harris * Update documentation/docs/20-core-concepts/50-state-management.md --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .../20-core-concepts/50-state-management.md | 139 +++++++++++++++--- packages/kit/src/runtime/app/stores.js | 2 +- 2 files changed, 118 insertions(+), 23 deletions(-) diff --git a/documentation/docs/20-core-concepts/50-state-management.md b/documentation/docs/20-core-concepts/50-state-management.md index a31c39d9deb6..ead654873526 100644 --- a/documentation/docs/20-core-concepts/50-state-management.md +++ b/documentation/docs/20-core-concepts/50-state-management.md @@ -2,15 +2,47 @@ title: State management --- -Managing state is one of the hardest parts in application development. This section covers various use cases with regards to state management and what to watch out for. +If you're used to building client-only apps, state management in an app that spans server and client might seem intimidating. This section provides tips for avoiding some common gotchas. -## Avoid global state in SSR +## Avoid shared state on the server -If you are creating a [single page application (SPA)](glossary#spa) with SvelteKit, you can create global state freely, as you can be sure that it's only initialized inside the user's browser. If you use [SSR](glossary#ssr) however, you have to watch out for a couple of things when managing state. In many server environments, a single instance of your app will serve multiple users (this is not specific to SvelteKit - it's one of the gotchas of working with such environments). For that reason, per-request or per-user state must not be stored in global variables. +Browsers are _stateful_ — state is stored in memory as the user interacts with the application. Servers, on the other hand, are _stateless_ — the content of the response is determined entirely by the content of the request. -Consider the following example where the user is set from inside a `load` function: +Conceptually, that is. In reality, servers are often long-lived and shared by multiple users. For that reason it's important not to store data in shared variables. For example, consider this code: -```js +```js +// @errors: 7034 7005 +/// file: +page.server.js +let user; + +/** @type {import('./$types').PageServerLoad} */ +export function load() { + return { user }; +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request }) => { + const data = await request.formData(); + + // NEVER DO THIS! + user = { + name: data.get('name'), + embarrassingSecret: data.get('secret') + }; + } +} +``` + +The `user` variable is shared by everyone who connects to this server. If Alice submitted an embarrassing secret, and Bob visited the page after her, Bob would know Alice's secret. In addition, when Alice returns to the site later in the day, the server may have restarted, losing her data. + +Instead, you should _authenticate_ the user using [`cookies`](/docs/load#cookies-and-headers) and persist the data to a database. + +## 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: + +```js /// file: +page.js // @filename: ambient.d.ts declare module '$lib/user' { @@ -19,41 +51,58 @@ declare module '$lib/user' { // @filename: index.js // ---cut--- -// DON'T DO THIS! import { user } from '$lib/user'; /** @type {import('./$types').PageLoad} */ export async function load({ fetch }) { const response = await fetch('/api/user'); + + // NEVER DO THIS! user.set(await response.json()); } ``` -If you are using SSR, the `load` function will run on the server initially, which means that the whole server instance which serves _all_ requests from _all_ users has its `user` state set to the one just requested from the API. To scope this to a single user, you have a couple of options: +As with the previous example, this puts one user's information in a place that is shared by _all_ users. Instead, just return the data... -- if you need to access the state only inside server `load` functions, use [`locals`](hooks#server-hooks-handle) -- if you need to persist the state across reloads, but only need to access it inside `load` functions, use [`cookies` in server `load` functions](load#cookies-and-headers). If the state is more complex, safe a key to the state in the cookie to look it up in for example a database -- if you need to access and update the state inside components, use Svelte's [context feature](https://svelte.dev/docs#run-time-svelte-setcontext). That way, the state is scoped to components, which means they are not shared across different requests on the server. The drawback is that you can only access the context at component initialization, which may make interacting with the store value a little trickier if you want to do that outside of components. SvelteKit's stores from `$app/stores` for example are setup like this (which is why you may have encountered a related error message) +```diff +/// file: +page.js +export async function load({ fetch }) { + const response = await fetch('/api/user'); + ++ return { ++ user: await response.json() ++ }; +} +``` + +...and pass it around to the components that need it, or use [`$page.data`](/docs/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 + +You might wonder how we're able to use `$page.data` and other [app stores](/docs/modules#$app-stores) if we can't use our own stores. The answer is that app stores on the server use Svelte's [context API](https://learn.svelte.dev/tutorial/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: ```svelte -/// +layout.svelte +/// file: src/routes/+layout.svelte ``` ```svelte -/// +src/user/+page.svelte +/// file: src/routes/user/+page.svelte + +
+

{data.title}

+

Reading time: {Math.round(estimatedReadingTime)} minutes

+
+ +
{@html data.content}
+``` + +...then navigating from `/blog/my-short-post` to `/blog/my-long-post` won't cause the component to be destroyed and recreated. The `data` prop (and by extension `data.title` and `data.content`) will change, but because the code isn't re-running, `estimatedReadingTime` won't be recalculated. + +Instead, we need to make the value [_reactive_](https://learn.svelte.dev/tutorial/reactive-assignments): + +```diff +/// file: src/routes/blog/[slug]/+page.svelte + +``` + +Reusing components like this means that things like sidebar scroll state are preserved, and you can easily animate between changing values. However, if you do need to completely destroy and remount a component on navigation, you can use this pattern: + +```svelte +{#key $page.url.pathname} + +{/key} +``` -## Managing forms +## Storing state in the URL -When coming from a pure Svelte or JavaScript background, you might be used to handling all form interactions through JavaScript. This works well when JavaScript is available but results in unresponsive UI when it isn't (which may be [more often than you think](https://kryogenix.org/code/browser/everyonehasjs.html)). If this is a concern to you, leverage SvelteKit's [form actions](form-actions) instead. +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`. -## Leverage the URL as state +## Storing ephemeral state in snapshots -UI-only state like "is the accordion open" are ok to store as component-level state that does not survive page reloads. Other state such as selected filters on a shopping page are better stored inside the URL as query parameters. That way they survive reloads and are accessible inside `load` functions through the `url` property. +Some UI state, such as 'is the accordion open?', is disposable — if the user navigates away or refreshes the page, it doesn't matter if the state is lost. In some cases, you _do_ want the data to persist if the user navigates to a different page and comes back, but storing the state in the URL or in a database would be overkill. For this, SvelteKit provides [snapshots](/docs/snapshots), which let you associate component state with a history entry. \ No newline at end of file diff --git a/packages/kit/src/runtime/app/stores.js b/packages/kit/src/runtime/app/stores.js index e71467e9bad7..f93d8e6349de 100644 --- a/packages/kit/src/runtime/app/stores.js +++ b/packages/kit/src/runtime/app/stores.js @@ -67,7 +67,7 @@ function get_store(name) { } catch (e) { throw new Error( `Cannot subscribe to '${name}' store on the server outside of a Svelte component, as it is bound to the current request via component context. This prevents state from leaking between users.` + - 'For more information, see https://kit.svelte.dev/docs/state-management#avoid-global-state-in-ssr' + 'For more information, see https://kit.svelte.dev/docs/state-management#avoid-shared-state-on-the-server' ); } }