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 all 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?

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
170 changes: 170 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,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
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';

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

// Create a store and update it when necessary...
const user = writable();
$: user.set(data.user);

// ...and add it to the context for child components to access
setContext('user', user);
</script>
```

```svelte
/// file: src/routes/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'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
<script>
/** @type {import('./$types').PageData} */
export let data;

// THIS CODE IS BUGGY!
const wordCount = data.content.split(' ').length;
const estimatedReadingTime = wordCount / 250;
</script>

<header>
<h1>{data.title}</h1>
<p>Reading time: {Math.round(estimatedReadingTime)} minutes</p>
</header>

<div>{@html data.content}</div>
```

...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
<script>
/** @type {import('./$types').PageData} */
export let data;

+ $: wordCount = data.content.split(' ').length;
+ $: estimatedReadingTime = wordCount / 250;
</script>
```

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}
<BlogPost title={data.title} content={data.title} />
{/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 `<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`.

## 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.
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-shared-state-on-the-server'
);
}
}
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