Skip to content

Commit 53e168f

Browse files
dummdidummbenmccanntomeckoHarryAllen1Rich-Harris
authored
docs: state management (#8547)
* docs: state management - docs about state management - more helpful error message when trying to run a SvelteKit store on the server closes #8524 * fix * fix the fix * update * fix link * Update documentation/docs/20-core-concepts/60-state-management.md Co-authored-by: Ben McCann <[email protected]> * Update packages/kit/src/runtime/app/stores.js Co-authored-by: Tomasz Olędzki <[email protected]> * move details to form actions, add url section * shorten, do not use form in example * remove now obsolete paragraph, add info on when params and url can change * links * changeset, link fix * info on when which load function runs * fix * closes #8302 * Update documentation/docs/20-core-concepts/50-state-management.md Co-authored-by: Ben McCann <[email protected]> * details, example * Update documentation/docs/20-core-concepts/20-load.md Co-authored-by: Harry Allen <[email protected]> * Update documentation/docs/20-core-concepts/20-load.md * Update documentation/docs/20-core-concepts/50-state-management.md * 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 <[email protected]> * Update documentation/docs/20-core-concepts/50-state-management.md --------- Co-authored-by: Simon H <[email protected]> --------- Co-authored-by: Ben McCann <[email protected]> Co-authored-by: Tomasz Olędzki <[email protected]> Co-authored-by: Harry Allen <[email protected]> Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 9a45f36 commit 53e168f

File tree

5 files changed

+209
-9
lines changed

5 files changed

+209
-9
lines changed

documentation/docs/20-core-concepts/20-load.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ As we've seen, there are two types of `load` function:
162162

163163
Conceptually, they're the same thing, but there are some important differences to be aware of.
164164

165+
### When does which load function run?
166+
167+
Server `load` functions _always_ run on the server.
168+
169+
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.
170+
171+
A `load` function is invoked at runtime, unless you [prerender](page-options#prerender) the page — in that case, it's invoked at build time.
172+
165173
### Input
166174

167175
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
226234
- it can be used to make credentialed requests on the server, as it inherits the `cookie` and `authorization` headers for the page request
227235
- it can make relative requests on the server (ordinarily, `fetch` requires a URL with an origin when used in a server context)
228236
- 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
229-
- 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.
237+
- 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.
230238

231239
```js
232240
/// 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
477485

478486
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.
479487

480-
## Invalidation
488+
## Rerunning load functions
481489

482490
SvelteKit tracks the dependencies of each `load` function to avoid re-running it unnecessarily during navigation.
483491

@@ -578,11 +586,9 @@ To summarize, a `load` function will re-run in the following situations:
578586
- 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)
579587
- All active `load` functions were forcibly re-run with [`invalidateAll()`](modules#$app-navigation-invalidateall)
580588

581-
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.
582-
583-
## Shared state
589+
`params` and `url` can change in response to a `<a href="..">` link click, a [`<form>` interaction](form-actions#get-vs-post), a [`goto`](modules#$app-navigation-goto) invocation, or a [`redirect`](modules#sveltejs-kit-redirect).
584590

585-
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`.
591+
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.
586592

587593
## Further reading
588594

documentation/docs/20-core-concepts/30-form-actions.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,30 @@ const response = await fetch(this.action, {
456456

457457
## Alternatives
458458

459-
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.
459+
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:
460+
461+
```svelte
462+
/// file: send-message/+page.svelte
463+
<script>
464+
function rerun() {
465+
fetch('/api/ci', {
466+
method: 'POST'
467+
});
468+
}
469+
</script>
470+
471+
<button on:click={rerun}>Rerun CI</button>
472+
```
473+
474+
```js
475+
// @errors: 2355 1360
476+
/// file: api/ci/+server.js
477+
478+
/** @type {import('./$types').RequestHandler} */
479+
export function POST() {
480+
// do something
481+
}
482+
```
460483

461484
## GET vs POST
462485

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
---
2+
title: State management
3+
---
4+
5+
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.
6+
7+
## Avoid shared state on the server
8+
9+
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.
10+
11+
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:
12+
13+
```js
14+
// @errors: 7034 7005
15+
/// file: +page.server.js
16+
let user;
17+
18+
/** @type {import('./$types').PageServerLoad} */
19+
export function load() {
20+
return { user };
21+
}
22+
23+
/** @type {import('./$types').Actions} */
24+
export const actions = {
25+
default: async ({ request }) => {
26+
const data = await request.formData();
27+
28+
// NEVER DO THIS!
29+
user = {
30+
name: data.get('name'),
31+
embarrassingSecret: data.get('secret')
32+
};
33+
}
34+
}
35+
```
36+
37+
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.
38+
39+
Instead, you should _authenticate_ the user using [`cookies`](/docs/load#cookies-and-headers) and persist the data to a database.
40+
41+
## No side-effects in load
42+
43+
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:
44+
45+
```js
46+
/// file: +page.js
47+
// @filename: ambient.d.ts
48+
declare module '$lib/user' {
49+
export const user: { set: (value: any) => void };
50+
}
51+
52+
// @filename: index.js
53+
// ---cut---
54+
import { user } from '$lib/user';
55+
56+
/** @type {import('./$types').PageLoad} */
57+
export async function load({ fetch }) {
58+
const response = await fetch('/api/user');
59+
60+
// NEVER DO THIS!
61+
user.set(await response.json());
62+
}
63+
```
64+
65+
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...
66+
67+
```diff
68+
/// file: +page.js
69+
export async function load({ fetch }) {
70+
const response = await fetch('/api/user');
71+
72+
+ return {
73+
+ user: await response.json()
74+
+ };
75+
}
76+
```
77+
78+
...and pass it around to the components that need it, or use [`$page.data`](/docs/load#$page-data).
79+
80+
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.
81+
82+
## Using stores with context
83+
84+
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:
85+
86+
```svelte
87+
/// file: src/routes/+layout.svelte
88+
<script>
89+
import { setContext } from 'svelte';
90+
import { writable } from 'svelte/store';
91+
92+
/** @type {import('./$types').LayoutData} */
93+
export let data;
94+
95+
// Create a store and update it when necessary...
96+
const user = writable();
97+
$: user.set(data.user);
98+
99+
// ...and add it to the context for child components to access
100+
setContext('user', user);
101+
</script>
102+
```
103+
104+
```svelte
105+
/// file: src/routes/user/+page.svelte
106+
<script>
107+
import { getContext } from 'svelte';
108+
109+
// Retrieve user store from context
110+
const user = getContext('user');
111+
</script>
112+
113+
<p>Welcome {$user.name}</p>
114+
```
115+
116+
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.
117+
118+
## Component state is preserved
119+
120+
When you navigate around your application, SvelteKit reuses existing layout and page components. For example, if you have a route like this...
121+
122+
```svelte
123+
/// file: src/routes/blog/[slug]/+page.svelte
124+
<script>
125+
/** @type {import('./$types').PageData} */
126+
export let data;
127+
128+
// THIS CODE IS BUGGY!
129+
const wordCount = data.content.split(' ').length;
130+
const estimatedReadingTime = wordCount / 250;
131+
</script>
132+
133+
<header>
134+
<h1>{data.title}</h1>
135+
<p>Reading time: {Math.round(estimatedReadingTime)} minutes</p>
136+
</header>
137+
138+
<div>{@html data.content}</div>
139+
```
140+
141+
...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.
142+
143+
Instead, we need to make the value [_reactive_](https://learn.svelte.dev/tutorial/reactive-assignments):
144+
145+
```diff
146+
/// file: src/routes/blog/[slug]/+page.svelte
147+
<script>
148+
/** @type {import('./$types').PageData} */
149+
export let data;
150+
151+
+ $: wordCount = data.content.split(' ').length;
152+
+ $: estimatedReadingTime = wordCount / 250;
153+
</script>
154+
```
155+
156+
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:
157+
158+
```svelte
159+
{#key $page.url.pathname}
160+
<BlogPost title={data.title} content={data.title} />
161+
{/key}
162+
```
163+
164+
## Storing state in the URL
165+
166+
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 `<a href="...">` or `<form action="...">` 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`.
167+
168+
## Storing ephemeral state in snapshots
169+
170+
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.

packages/kit/src/runtime/app/stores.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ function get_store(name) {
6666
return getStores()[name];
6767
} catch (e) {
6868
throw new Error(
69-
`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.`
69+
`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.` +
70+
'For more information, see https://kit.svelte.dev/docs/state-management#avoid-shared-state-on-the-server'
7071
);
7172
}
7273
}

packages/kit/types/ambient.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ declare module '$app/navigation' {
195195
*/
196196
state?: any;
197197
/**
198-
* If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#invalidation for more info on invalidation.
198+
* 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.
199199
*/
200200
invalidateAll?: boolean;
201201
}

0 commit comments

Comments
 (0)