Skip to main content
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 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.
It’s important not to store data in shared variables on the server. Servers are often long-lived and shared by multiple users.

The problem

Consider this buggy code:
/// file: +page.server.js
let user;

/** @type {import('./$types').PageServerLoad} */
export function load() {
	return { user };
}

/** @satisfies {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!

The solution

Instead, you should authenticate the user using cookies and persist the data to a database:
1

Use cookies for authentication

export async function load({ cookies }) {
	const sessionid = cookies.get('sessionid');
	return {
		user: await db.getUser(sessionid)
	};
}
2

Store data in a database

export const actions = {
	default: async ({ cookies, request }) => {
		const sessionid = cookies.get('sessionid');
		const data = await request.formData();
		
		await db.updateUser(sessionid, {
			name: data.get('name'),
			secret: data.get('secret')
		});
	}
}

No side-effects in load

For the same reason, your load functions should be pure — no side-effects (except maybe the occasional console.log(...)).

Bad: Writing to global state

/// file: +page.js
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());
}

Good: Returning data

/// file: +page.js
/** @type {import('./$types').PageLoad} */
export async function load({ fetch }) {
	const response = await fetch('/api/user');

	return {
		user: await response.json()
	};
}
Pass the data to components that need it, or use page.data.

Using state and stores with context

You might wonder how we’re able to use page.data and other app state if we can’t use global state. The answer is that app state on the server uses Svelte’s context API.

Creating context-based state

<!--- file: src/routes/+layout.svelte --->
<script>
	import { setContext } from 'svelte';

	/** @type {import('./$types').LayoutProps} */
	let { data } = $props();

	// Pass a function referencing our state
	// to the context for child components to access
	setContext('user', () => data.user);
</script>
We’re passing a function into setContext to keep reactivity across boundaries. Read more about this in the Svelte documentation on passing state into functions.

Reactivity considerations

Updating the value of context-based state in deeper-level pages or components while the page is being rendered via SSR will not affect the value in the parent component.In contrast, on the client the value will be propagated. To avoid values ‘flashing’ during hydration, it’s generally recommended to pass state down into components rather than up.

Component and page state is preserved

When you navigate around your application, SvelteKit reuses existing layout and page components.

The problem

Consider this buggy code:
<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
	/** @type {import('./$types').PageProps} */
	let { data } = $props();

	// 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>
Navigating from /blog/my-short-post to /blog/my-long-post won’t cause the component to be destroyed and recreated. The data prop will update, but wordCount and estimatedReadingTime won’t be recalculated.

The solution

Make the values reactive using $derived:
/// file: src/routes/blog/[slug]/+page.svelte
<script>
	/** @type {import('./$types').PageProps} */
	let { data } = $props();

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

Force component recreation

If you need to completely destroy and remount a component on navigation:
<script>
	import { page } from '$app/state';
</script>

{#key page.url.pathname}
	<BlogPost title={data.title} content={data.title} />
{/key}
If your code in onMount and onDestroy needs to run again after navigation, use afterNavigate and beforeNavigate respectively.

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 are a good place to put them.

Using search params

Accessing in load functions

/// file: +page.js
export function load({ url }) {
	const sort = url.searchParams.get('sort');
	const order = url.searchParams.get('order');
	
	return {
		sort,
		order
	};
}

Accessing in components

<script>
	import { page } from '$app/state';
	
	const sort = $derived(page.url.searchParams.get('sort'));
	const order = $derived(page.url.searchParams.get('order'));
</script>

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. For this, SvelteKit provides snapshots:
1

Export snapshot object

<script>
	let expanded = $state(false);

	export const snapshot = {
		capture: () => expanded,
		restore: (value) => expanded = value
	};
</script>
2

User navigates away

The capture function is called, storing the current state
3

User navigates back

The restore function is called with the stored value
Snapshots let you associate component state with a history entry without storing it in the URL or database.

State management strategies

For different types of state, use different strategies:

Authentication state

Use cookies and event.locals

Page data

Use load functions and return data

Filters and sorting

Use URL search parameters

UI state

Use Svelte state and context

Ephemeral history

Use snapshots

Global client state

Use context or stores (only if not using SSR)

Next steps