Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: state management #8547

Merged
merged 26 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4353400
docs: state management
dummdidumm Jan 16, 2023
b198f59
fix
dummdidumm Jan 16, 2023
c1ffd0b
fix the fix
dummdidumm Jan 16, 2023
d66695b
Merge branch 'master' into state-management
dummdidumm Jan 24, 2023
dba26f3
update
dummdidumm Jan 24, 2023
7201509
fix link
dummdidumm Jan 24, 2023
53285d9
Update documentation/docs/20-core-concepts/60-state-management.md
dummdidumm Jan 25, 2023
978ac41
Update packages/kit/src/runtime/app/stores.js
dummdidumm Jan 27, 2023
f14481c
Merge branch 'master' into state-management
dummdidumm Feb 8, 2023
fed53b2
move details to form actions, add url section
dummdidumm Feb 8, 2023
044e3cb
shorten, do not use form in example
dummdidumm Feb 8, 2023
98c9b13
remove now obsolete paragraph, add info on when params and url can ch…
dummdidumm Feb 8, 2023
bd87958
links
dummdidumm Feb 8, 2023
aecbc2b
changeset, link fix
dummdidumm Feb 8, 2023
a18f31f
info on when which load function runs
dummdidumm Feb 8, 2023
1d123cb
fix
dummdidumm Feb 8, 2023
2e69c32
closes #8302
dummdidumm Feb 8, 2023
7f8f69e
Merge branch 'master' into state-management
dummdidumm Feb 9, 2023
e908196
Update documentation/docs/20-core-concepts/50-state-management.md
dummdidumm Feb 9, 2023
99a8907
details, example
dummdidumm Feb 9, 2023
44773f5
Merge branch 'state-management' of https://github.com/sveltejs/kit in…
dummdidumm Feb 9, 2023
29a8a4c
Update documentation/docs/20-core-concepts/20-load.md
dummdidumm Feb 16, 2023
8798504
merge master
Rich-Harris Feb 28, 2023
42b8db8
Update documentation/docs/20-core-concepts/20-load.md
Rich-Harris Feb 28, 2023
b7e90e3
Update documentation/docs/20-core-concepts/50-state-management.md
Rich-Harris Feb 28, 2023
87c06c5
docs: state management part 2 (#9239)
Rich-Harris Feb 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions documentation/docs/20-core-concepts/20-load.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved

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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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 `<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).

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

Expand Down
25 changes: 24 additions & 1 deletion documentation/docs/20-core-concepts/30-form-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<script>
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>

<button on:click={rerun}>Rerun CI</button>
```

```js
// @errors: 2355 1360
/// file: api/ci/+server.js

/** @type {import('./$types').RequestHandler} */
export function POST() {
// do something
}
```

## GET vs POST

Expand Down
75 changes: 75 additions & 0 deletions documentation/docs/20-core-concepts/50-state-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: State Management
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
---

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 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:

```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 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
<script>
import { setContext } from 'svelte';

/** @type {import('./$types').LayoutData} */
export let data;

// Create a store...
const user = writable(data.user);
// ...add it to the context for child components to access
setContext('user', user);
// Optionally update the data everytime the load function is rerun
$: $user = data.user;
</script>
```

```svelte
/// +src/user/+page.svelte
<script>
import { getContext } from 'svelte';

// Retrieve user store from context
const user = getContext('user');
</script>

<p>Welcome {$user.name}</p>
```

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this belong here? It's well covered by the form section, and I don't think of mutations as part of state management

Suggested change
## 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutating state is an integral part of managing state to me. If all state was just readonly, managing it would be a lot easier 😄


## 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.
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/app/stores.js
Original file line number Diff line number Diff line change
Expand Up @@ -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-global-state-in-ssr'
);
}
}
2 changes: 1 addition & 1 deletion packages/kit/types/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down