Sharing State Across Svelte Components with Stores: A Task Guide
Learn how to create shared Svelte stores, subscribe with the $ prefix, update immutably, and avoid SSR and memory pitfalls.
01 Jan 2026, 00:39 UTC

Desired outcome
Passing props through five layers of components to update a single badge is a sign that state belongs in a store. In Svelte, a store is an external reactive object you can import into any component, subscribe to with the $ prefix, and update through set or update. This guide walks through creating a shared store, subscribing safely, handling async data, and avoiding the SSR and memory pitfalls that catch teams during migration.
Prerequisites
- A Svelte project using Svelte 4.x or Svelte 5.x. The
svelte/storeAPI is available in both versions. - Familiarity with ES module imports and Svelte component lifecycle.
- If you are using SvelteKit, an understanding that
loadfunctions and server hooks run per request.
Create a shared store module
Create a dedicated module and export a writable store initialized with a default value. Because ES modules are singletons, every file that imports this module receives the same store instance rather than a copy.
// src/lib/stores/user.js
import { writable, derived } from 'svelte/store';
export const user = writable({ name: 'Guest', status: 'idle' });
export const isLoggedIn = derived(user, $user => $user.status === 'authenticated');
The derived store recomputes automatically whenever user changes, so components can read $isLoggedIn without duplicating the comparison logic.
Subscribe with the $store syntax
Inside a .svelte file, prefix an imported store with $ at the top level of the script or directly in the markup. Svelte compiles this into an automatic subscription and unsubscribes when the component is destroyed.
<!-- Header.svelte -->
<script>
import { user, isLoggedIn } from '$lib/stores/user.js';
</script>
<p>Hello, {$user.name}</p>
{#if $isLoggedIn}
<button>Log out</button>
{/if}
Limitation: The $store auto-subscription is only valid in the component's reactive context. Inside an ordinary function or event handler, you must call store.subscribe() manually and invoke the returned unsubscribe function when finished.
Update state immutably
Never mutate the value held by a store directly. Svelte's store contract relies on reference equality to decide whether to notify subscribers. Use set to replace the entire value or update to derive a new one from the current state, always returning a fresh object or array.
// Inside an event handler or action
import { user } from '$lib/stores/user.js';
function login(name) {
user.update(current => ({ ...current, name, status: 'authenticated' }));
}
If you write $user.name = 'Alice', the UI will not update because subscribers never receive a change notification.
Model asynchronous data with a predictable contract
For data that arrives over time, wrap a writable store with explicit loading, error, and data fields. This gives every consumer a stable shape to render against, whether the request is pending, failed, or complete.
// src/lib/stores/profile.js
import { writable } from 'svelte/store';
export function createProfileStore() {
const { subscribe, set } = writable({ loading: true, error: null, data: null });
return {
subscribe,
async load(id) {
try {
const res = await fetch(`/api/profile/${id}`);
if (!res.ok) throw new Error(res.statusText);
const data = await res.json();
set({ loading: false, error: null, data });
} catch (error) {
set({ loading: false, error: error.message, data: null });
}
}
};
}
export const profile = createProfileStore();
In a component, trigger the load in onMount and render each branch of the contract:
<script>
import { profile } from '$lib/stores/profile.js';
import { onMount } from 'svelte';
onMount(() => profile.load('123'));
</script>
{#if $profile.loading}
<p>Loading…</p>
{:else if $profile.error}
<p>Error: {$profile.error}</p>
{:else}
<p>{$profile.data.name}</p>
{/if}
Keep request state isolated during SSR
In SvelteKit, a module-scope store is instantiated once per server process. If you seed it with request-specific data—such as a session identifier—that value can bleed between users. To isolate request state, create the store inside the component tree and share it through Svelte's context API rather than exporting a singleton from a module.
<!-- +page.svelte -->
<script>
import { writable } from 'svelte/store';
import { setContext } from 'svelte';
export let data;
const requestStore = writable({ requestId: data.requestId });
setContext('requestStore', requestStore);
</script>
<slot />
A child component can then read it with getContext('requestStore') and subscribe via $requestStore.
Expected checks
- Two-component propagation test. Create a writer component that calls
user.update()and a separate reader component that displays$user.name. Confirm the reader updates immediately without any prop drilling. - Subscription cleanup check. In a test harness, repeatedly mount and unmount a subscriber component. Open the browser DevTools Memory panel and record a heap timeline. If memory grows linearly with mount cycles, you likely have a leaked manual subscription.
- Version compatibility check. If you are migrating from Svelte 4 to Svelte 5, verify that existing
$storesubscriptions still update as expected. Svelte 5 introduces$stateand$derivedrunes as the idiomatic primitives, but the store API remains supported. Compare behavior in both versions before committing to a mixed pattern.
Recovery options
- UI does not update after a change. Check whether you mutated the stored value directly instead of calling
setorupdate. Replace the mutation with an immutable update that returns a new reference. - Memory grows after navigation. Ensure you are not creating manual subscriptions inside functions without calling the returned unsubscribe function. Prefer the
$storeauto-subscription in templates, which Svelte cleans up automatically. - Server-rendered pages show another user's data. Move request-specific initialization out of module scope and into per-request lifecycle points such as component initialization or SvelteKit hooks, then share the instance via context.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.