Choosing Between Svelte 5 Runes, Stores, and Context for Shared State
A decision guide for Svelte 5 developers comparing runes, legacy stores, and component context for shared reactive state, with SSR validation steps.
25 Jun 2026, 05:04 UTC

The decision you actually face
If you are maintaining a SvelteKit application and need to share reactive state across routes or components, Svelte 5 gives you three supported paths: runes in .svelte.ts modules, legacy stores, and component context. The choice is not stylistic. It determines whether your state survives server-side rendering (SSR) without a provider component, whether you can interoperate with Svelte 4 libraries, and how much boilerplate you maintain. This guide compares the constraints and trade-offs, then shows a runes-based implementation with SSR validation.
Constraints that narrow the choice
Before comparing options, confirm three constraints. First, your svelte package version: runes require Svelte 5.x and a compatible compiler. Second, your SSR requirement: does the state need to render on the server before hydration? Third, your library surface: do your dependencies still export Svelte 4 stores? These factors eliminate options before you write code.
Comparing the three supported options
| Dimension | Runes (.svelte.ts) | Legacy stores | Component context |
|---|---|---|---|
| SSR survival | Renders on server as module singleton | Needs provider or manual SSR handling | Invisible outside its parent tree |
| Scope | App-wide via direct import | App-wide via $ auto-subscription | Component subtree only |
| Boilerplate | Low: no provider, direct import | Medium: $ prefix in components, manual unsubscribe outside | Low: no global namespace |
| Library interop | Requires Svelte 5 compiler; mixing with Svelte 4 components creates two reactivity systems | Fully supported; natural fit for Svelte 4 libraries | Must re-establish per subtree |
| Reactivity model | Fine-grained signals | Coarser subscription model | Not reactive across dynamic boundaries unless context holds reactive state |
Trade-offs that matter in practice
Runes
Runes offer fine-grained reactivity and SSR-safe singletons with no provider component, but they require a Svelte 5 compiler transform. If your app still mounts Svelte 4 components, those components use the legacy reactivity system; avoid converting hot paths until all components in the route are Svelte 5.
Stores
Stores remain fully supported and are the right choice when integrating with Svelte-4-era libraries or needing writable contracts. The trade-off is ceremony: the $ prefix auto-subscribes inside components, but outside components you must manage subscriptions manually, and derived state adds extra layers.
Context
Context is simplest for feature-local state and avoids a global namespace, but it is invisible outside its parent tree and cannot cross route boundaries in SvelteKit. It also must be re-established for each component subtree.
A concrete runes implementation
Create a module at src/lib/state.svelte.ts:
export const user = $state({
name: 'Ada',
role: 'admin'
});
export const greet = $derived(`Hello, ${user.name}`);
Import it directly into any component without stores or providers:
<script>
import { user, greet } from '$lib/state.svelte';
</script>
<p>{greet}</p>
<p>Role: {user.role}</p>
Existing writable stores can be wrapped in a .svelte.ts file using $state for new code, but avoid converting hot paths until all components in the route are Svelte 5.
Validating SSR behavior
Run these commands in your project root. None require elevated permissions beyond normal user access.
npm list svelte— confirm the installed version is 5.x. If the output shows 4.x, runes are unavailable.npx svelte-check— requiressvelte-checkas a dev dependency. Expect no errors about invalid rune placement. Risk: an outdatedsvelte-checkmay report false positives.npm run build— requires a SvelteKit project. Expect successful SSR compilation. Risk: SSR failures if the module is imported inside a client-only guard.npm run preview— start the preview server, open the route, then disable JavaScript in the browser or view page source. Expected check: the server-rendered HTML contains the derived string before hydration.
Limitations and migration cautions
- Do not call
$stateor$derivedinside.sveltefiles outside the top level of a.svelte.tsmodule; runes are only valid in.svelteand.svelte.ts/.svelte.jsfiles. - Mixing runes and stores in the same component can cause subtle update-timing differences because runes use fine-grained signals while stores use a coarser subscription model.
- Context values are not reactive across dynamic component boundaries unless the context itself holds reactive state.
$inspectis a development tool; remove it before production builds.
A practical way to check the result
After building, open the preview URL with JavaScript disabled. If the derived text appears, the state is SSR-compatible. If it appears only after hydration, the module is likely imported in a client-only context.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.