Sharing State
Two components need the same data: the search box and the results list, the cart icon and the cart page. Svelte's answers are ranked like every framework's - keep it local, lift it, then reach for the shared mechanisms - but the shared tier has a distinctly Svelte flavor: state lives in compiled modules, not in a bolted-on store library. That's a genuine convenience with one sharp edge, and this phase covers both.
First resort: lift it
<!-- SearchPage.svelte -->
What just happened: the state lives in the closest common parent; one child binds it (a
$bindable input component, phase 4), the other reads it as a prop. One source of truth, and the
two can never disagree. ({query} is shorthand for query={query} - you'll see it everywhere.)
When two components with separate copies of "the same" state drift apart, this is the refactor:
move it up, pass it down, delete the copies.
The Svelte move: state in a .svelte.js module
For state whose readers are scattered across the app - the cart, the session, preferences - phase 2's compiled-module trick becomes the architecture:
// src/lib/cart.svelte.js
export const cart = ;
export
export
<!-- any component, anywhere -->
Cart ({itemCount()})
What just happened: a plain module exports a $state object and the functions that mutate it.
Every importer sees the same object; a mutation from the product page updates the header's badge,
because the badge's markup reads through the same proxy. State plus its update logic in one
importable file - most of what a store library exists for, in vanilla Svelte. (If you've read the
Vue guide: this is a Pinia store's shape, without Pinia.)
⚠️ Gotcha - the export rule. Notice the module exports an object and mutates its properties. Export a reassignable primitive instead and you hit a wall:
// counter.svelte.js
export let count = ; // compile error: cannot export reassigned state
export
Why the compiler refuses: importers of a rebound let would capture a stale binding - the exact
dead-snapshot problem from phase 2, at module scale - so Svelte makes it a build error instead of
a silent bug. Two clean shapes exist: wrap in an object and mutate properties
(export const counter = $state({ value: 0 })), or keep state private and export accessor
functions. Either way, reads go through something the proxy can intercept.
Context: per-tree state without prop threading
Module state is app-global - one cart for everyone. Sometimes you want one instance per subtree: this form's state shared by its field components, this accordion group's open-item tracker. That's the context API:
<!-- Accordion.svelte -->
{@render children()}
<!-- AccordionItem.svelte, any depth below -->
accordion.openId = accordion.openId === id ? null : id}>
{@render children()}
What just happened: the parent placed a reactive object into context; descendants at any depth
retrieved the same object - no prop threading through layers that don't care. Two accordions on
one page each provide their own context, so their states don't collide - the thing module state
can't do. Rules worth knowing: setContext/getContext run during component setup only (not in
handlers or effects), and lookups walk up the tree - siblings can't see each other's context.
What about stores?
Pre-runes Svelte shared state through stores - writable(0), subscribed with a $store
prefix in markup. They still work, they're all over existing codebases (including this site's),
and a few niches still want them (interop with libraries built on the store contract). But for new
code, module $state plus context covers the territory with fewer concepts. Translation for
reading legacy: writable(x) ≈ module $state, $storeName in markup ≈ reading the state
object, derived(...) ≈ $derived.
Choosing, in one table
| Situation | Reach for |
|---|---|
| One component cares | $state right there |
| Siblings need it | Lift to the common parent |
| A widget tree needs its own instance | Context |
| The whole app shares one instance | .svelte.js module state |
| Legacy code / store-based libraries | Stores - read fluently, write runes |
Recap
- Lift first: closest common parent, props/bindings down. Two copies of one truth always drift.
.svelte.jsmodules hold app-wide state as$state+ mutation functions - store-library ergonomics, zero dependencies.- Never export reassignable state - export an object you mutate, or accessor functions; the compiler enforces it.
- Context = per-subtree instances, set during component setup, visible only downward.
- Stores are the legacy tier: recognize
writable/$storeon sight, prefer runes for new code.
[
{
"q": "export let count = $state(0) in a .svelte.js module fails to compile. What's the compiler protecting you from?",
"choices": [
"Primitives can't be reactive in modules - only objects can",
"Importers would capture a stale binding when count is reassigned - the dead-snapshot bug, promoted to module scale",
"Module state must be read-only by design",
"$state is not allowed in .svelte.js files"
],
"answer": 1,
"why": [
"A primitive is fine as long as nothing reassigns the exported binding - the object wrapper exists to give mutations an interceptable home.",
null,
"Module state is meant to be mutated - through object properties or exported functions.",
".svelte.js files exist precisely to host runes."
],
"explain": "Reassigning an exported let would leave importers holding yesterday's value with no proxy in the path. The build error replaces what would otherwise be phase 2's silent disconnection."
},
{
"q": "Two independent Wizard components on one page each need their own shared step-state for their child panels. Module state or context?",
"choices": [
"Module state - it's the modern Svelte way to share",
"Context - each Wizard provides its own instance to its own subtree",
"Either works identically",
"Neither - the panels should use $bindable props"
],
"answer": 1,
"why": [
"One module = one instance app-wide: both wizards would fight over the same step counter.",
null,
"They differ exactly here - global singleton versus per-tree instance.",
"Bindable props work for direct parent-child pairs, but threading through every panel layer is the problem context removes."
],
"explain": "Module state is a singleton; context is scoped to the providing component's subtree. Multiple instances of a widget each needing private shared state is the context case."
}
]
← Phase 4: Components: Props, Callbacks, and Snippets · Guide overview · Phase 6: Effects, Lifecycle, and Fetching →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 2 questions
1. export let count = $state(0) in a .svelte.js module fails to compile. What's the compiler protecting you from?
2. Two independent Wizard components on one page each need their own shared step-state for their child panels. Module state or context?