diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index 08ac39f4185a..f8937247e465 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. @@ -226,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 @@ -477,7 +485,7 @@ On platforms that do not support streaming, such as AWS Lambda, responses will b 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. @@ -578,11 +586,9 @@ 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. ## Further reading diff --git a/documentation/docs/20-core-concepts/30-form-actions.md b/documentation/docs/20-core-concepts/30-form-actions.md index 9cd481b9cb54..b8c2ba57c81a 100644 --- a/documentation/docs/20-core-concepts/30-form-actions.md +++ b/documentation/docs/20-core-concepts/30-form-actions.md @@ -456,7 +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. +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 +// @errors: 2355 1360 +/// file: api/ci/+server.js + +/** @type {import('./$types').RequestHandler} */ +export function POST() { + // do something +} +``` ## 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..ead654873526 --- /dev/null +++ b/documentation/docs/20-core-concepts/50-state-management.md @@ -0,0 +1,170 @@ +--- +title: State management +--- + +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 shared state on the server + +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. + +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 +// @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' { + export const user: { set: (value: any) => void }; +} + +// @filename: index.js +// ---cut--- +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()); +} +``` + +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... + +```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 +/// file: src/routes/+layout.svelte + +``` + +```svelte +/// file: src/routes/user/+page.svelte + + +

Welcome {$user.name}

+``` + +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. + +## Component state is preserved + +When you navigate around your application, SvelteKit reuses existing layout and page components. For example, if you have a route like this... + +```svelte +/// file: src/routes/blog/[slug]/+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} +``` + +## 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`. + +## Storing ephemeral state in snapshots + +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 67727a119d82..f93d8e6349de 100644 --- a/packages/kit/src/runtime/app/stores.js +++ b/packages/kit/src/runtime/app/stores.js @@ -66,7 +66,8 @@ function get_store(name) { return getStores()[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.` + `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-shared-state-on-the-server' ); } } diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts index fd6e11558361..722746c08c53 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; }