You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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
6
7
-
## Avoid global state in SSR
7
+
## Avoid shared state on the server
8
8
9
-
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.
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
10
11
-
Consider the following example where the user is set from inside a `load` function:
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
12
13
-
```js
13
+
```js
14
+
// @errors: 7034 7005
15
+
/// file: +page.server.js
16
+
let user;
17
+
18
+
/**@type{import('./$types').PageServerLoad}*/
19
+
exportfunctionload() {
20
+
return { user };
21
+
}
22
+
23
+
/**@type{import('./$types').Actions}*/
24
+
exportconstactions= {
25
+
default:async ({ request }) => {
26
+
constdata=awaitrequest.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
14
46
/// file: +page.js
15
47
// @filename: ambient.d.ts
16
48
declare module'$lib/user' {
@@ -19,41 +51,58 @@ declare module '$lib/user' {
19
51
20
52
// @filename: index.js
21
53
// ---cut---
22
-
// DON'T DO THIS!
23
54
import { user } from'$lib/user';
24
55
25
56
/**@type{import('./$types').PageLoad}*/
26
57
exportasyncfunctionload({ fetch }) {
27
58
constresponse=awaitfetch('/api/user');
59
+
60
+
// NEVER DO THIS!
28
61
user.set(awaitresponse.json());
29
62
}
30
63
```
31
64
32
-
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:
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...
33
66
34
-
- if you need to access the state only inside server `load` functions, use [`locals`](hooks#server-hooks-handle)
35
-
- 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
36
-
- 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)
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:
37
85
38
86
```svelte
39
-
/// +layout.svelte
87
+
/// file: src/routes/+layout.svelte
40
88
<script>
41
89
import { setContext } from 'svelte';
90
+
import { writable } from 'svelte/store';
42
91
43
92
/** @type {import('./$types').LayoutData} */
44
93
export let data;
45
94
46
-
// Create a store...
47
-
const user = writable(data.user);
48
-
// ...add it to the context for child components to access
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
49
100
setContext('user', user);
50
-
// Optionally update the data everytime the load function is rerun
51
-
$: $user = data.user;
52
101
</script>
53
102
```
54
103
55
104
```svelte
56
-
/// +src/user/+page.svelte
105
+
/// file: src/routes/user/+page.svelte
57
106
<script>
58
107
import { getContext } from 'svelte';
59
108
@@ -64,12 +113,58 @@ If you are using SSR, the `load` function will run on the server initially, whic
64
113
<p>Welcome {$user.name}</p>
65
114
```
66
115
67
-
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).
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...
...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:
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.
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`.
72
167
73
-
## Leverage the URL as state
168
+
## Storing ephemeral state in snapshots
74
169
75
-
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.
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.
Copy file name to clipboardExpand all lines: packages/kit/src/runtime/app/stores.js
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -67,7 +67,7 @@ function get_store(name) {
67
67
}catch(e){
68
68
thrownewError(
69
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-global-state-in-ssr'
70
+
'For more information, see https://kit.svelte.dev/docs/state-management#avoid-shared-state-on-the-server'
0 commit comments