Svelte 5 shared state: stores vs rune modules in a mixed SvelteKit codebase
0 reputation · 28 Jan 2022, 21:46 UTC
0 reputation · 28 Jan 2022, 21:46 UTC
We are mid-migration from Svelte 4 to Svelte 5 and need to settle one configuration decision before it spreads across the codebase: how shared client state should be declared going forward.
Today we have roughly a dozen Svelte stores (writable and derived) consumed with the $store auto-subscription syntax in components. Svelte 5 runes ($state, $derived) also work in .svelte.js modules, so shared reactive state no longer requires stores. The uncertainty is that the two idioms are not symmetric: $store auto-subscription does not apply inside .svelte.js modules, and interop helpers like fromStore only bridge in one direction. Some state is also consumed inside SvelteKit load results, where serialization strips class instances and non-JSON values.
We want to avoid ending up with two competing reactivity models that new contributors mix arbitrarily, but a full rewrite of every store is a large change we cannot validate quickly.
30775 reputation · 29 Jan 2022, 04:53 UTC
For shared client state that must be reachable from multiple routes, layout files, or SvelteKit load functions, keep it as a Svelte store. Use rune‑based state only when the state is truly private to a module or component.
Confirmed from the Svelte 5 documentation and community practice:
writable, readable, derived) are singleton reactive primitives that can be imported anywhere, survive server‑side rendering, and serialize cleanly in load functions.$state, $derived, $effect) are compile‑time declarations scoped to the file where they are defined unless explicitly exported. They are ideal for component‑local or module‑scoped reactivity.During an incremental migration, treat existing stores as the source of truth for any state that is already imported across files. Only refactor to runes when you can confirm the state is never accessed outside its defining module.
.svelte file, a +layout.svelte, a +page.svelte, or a SvelteKit load function → keep as a store..svelte.js module (or its sub‑modules) → consider moving to runes.$store usage unchanged..svelte.js file (e.g., src/lib/uiState.js) and declare the state with $state (or $derived for derived values).export const count = $state(0);$store syntax, keep a thin store wrapper that mirrors the rune:import { writable } from 'svelte/store';
export const count = $state(0);
export const countStore = writable(count);
$: countStore.set(count); // keep store in sync with rune
This wrapper ensures legacy $countStore subscriptions work without duplicating the source of truth.
count += 1) and check that all $countStore subscribers receive the new value.The recommended pattern is the thin wrapper shown above. It preserves a single source of truth (the rune) while providing a store‑compatible API for existing code. No duplication of state occurs because the store merely forwards the rune’s value via an $effect (the $: countStore.set(count) line).
There is no inherent performance penalty; Svelte batches updates within the same microtask. The only risk is accidental double‑subscription if both a store and a rune listen to the same source. Ensure that:
If you notice extra renders or unexpected ordering, check for duplicate subscriptions.
To finalize the recommendation, please confirm: Is the shared state accessed inside any SvelteKit load function? If yes, the state must remain a store to survive serialization and server‑side execution.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.