Skip to content

State Management

SvelteKit state management is mostly about where state should live in an app that spans server and client. Its rule of thumb1 is to keep server state request-scoped and avoid shared module state for per-user data. For Svelte 5 component reactivity, see Runes.

Do not put user state in server module globals

Section titled “Do not put user state in server module globals”

A browser is stateful, but a server process can be long-lived and shared by many users. A top-level variable in a server module is shared by everyone who hits that server process, and can also disappear when the process restarts. SvelteKit’s state management docs use this as the core warning: do not store per-user data in shared server variables.1

Do this instead:

  • authenticate users with cookies
  • persist durable user data in a database
  • return request-specific data from load
  • pass that data through props, context, or $app/state

Do not write to stores, globals, or other shared state inside load. Return data from load and let SvelteKit pass it to the page. This keeps SSR safe and makes the app easier to reason about.1

export async function load({ fetch }) {
const response = await fetch("/api/user");
return { user: await response.json() };
}

SvelteKit’s own app state uses Svelte context on the server so that state is attached to the component tree instead of a process-global singleton. You can use the same pattern for your own state. Pass a function through context to preserve reactivity across boundaries:1

src/routes/+layout.svelte
<script>
import { setContext } from 'svelte';
let { data } = $props();
setContext('user', () => data.user);
</script>
src/routes/user/+page.svelte
<script>
import { getContext } from 'svelte';
const user = getContext('user');
</script>
<p>Welcome {user().name}</p>

Prefer passing state down. During SSR, updating context state from a deeper component cannot change markup that a parent has already rendered; on the client, the parent can react to the new value, which can cause hydration flashes.1

SvelteKit preserves layout and page component instances across navigation. That means setup code in a component does not rerun just because data changed, and onMount/onDestroy do not rerun on every route change. Values derived from data must be reactive:1

<script>
let { data } = $props();
let wordCount = $derived(data.content.split(' ').length);
let estimatedReadingTime = $derived(wordCount / 250);
</script>

If a component must be destroyed and recreated on navigation, key it by URL:

<script>
import { page } from '$app/state';
</script>
{#key page.url.pathname}
<BlogPost title={data.title} content={data.content} />
{/key}

If state should survive reloads or affect SSR, put it in the URL. Filters, sorting, tabs, and pagination often belong in search params like ?sort=price&order=ascending. Read them in load through the url parameter or in components through page.url.searchParams.1

For disposable UI state that should survive back/forward navigation without becoming URL or database state, use SvelteKit snapshots.1

State kindPut it here
Durable user datadatabase, keyed by authenticated user
Request-specific server dataload return values
App tree state during SSRSvelte context or $app/state
URL-affecting stateURL search params
Disposable history-entry UI stateSvelteKit snapshots

For component-local state, derived values, component props, and browser effects, use Svelte 5 runes like $state, $derived, $props, and $effect; see Runes.

  1. SvelteKit, “State management”, SvelteKit documentation LLM text, https://svelte.dev/docs/kit/state-management/llms.txt 2 3 4 5 6 7 8

Measuring CO2 Website Carbon