Runes vs Stores
The same counter, two ways
Section titled “The same counter, two ways”Before Svelte 5, sharing reactive state across components meant a store. Now you can put $state in a module and export it. Runes work outside components — but only in files with a .svelte.js or .svelte.ts extension, which tells the compiler to enable rune processing.
The store version:
import { writable } from 'svelte/store';export const count = writable(0);<script> import { count } from './counter.js';</script><button onclick={() => count.update((n) => n + 1)}>{$count}</button>The runes version:
// counter.svelte.js ← note the .svelte.js extensionexport const counter = $state({ value: 0 });<script> import { counter } from './counter.svelte.js';</script><button onclick={() => counter.value++}>{counter.value}</button>No writable, no set/update, no $ prefix — you read and mutate counter.value like a normal object, and it’s reactive everywhere it’s used.
Why export an object, not a bare value
Section titled “Why export an object, not a bare value”Notice the runes version exports { value: 0 }, not 0. A bare exported let x = $state(0) can’t stay linked across a module boundary — importing it copies the value, losing reactivity. Exporting an object (or a function returning reactive state) keeps everyone pointing at the same reactive proxy, so mutations to counter.value propagate. This is the one gotcha of module-level runes.
Side by side
Section titled “Side by side”Store (writable) | Runes (.svelte.js) | |
|---|---|---|
| Read in component | $count | counter.value |
| Update | count.set(x) / count.update(fn) | counter.value = x |
| Outside a component | works anywhere | needs .svelte.js/.svelte.ts |
| The subscribe contract | yes (interop, observables) | no |
| Ceremony | more (methods, $) | less (plain read/write) |
flowchart TB q["Sharing reactive state?"] --> interop["Need the subscribe contract (observables, external push source)?"] interop --> yes["Yes: use a store"] interop --> no["No: export $state from a .svelte.js module"]
When to use each
Section titled “When to use each”- Runes (default). Most shared app state — a cart, a theme, a current-user object. Simpler, less boilerplate, and the same mental model as component-local state.
- Stores. When you need the contract: wrapping a WebSocket or other push source with
readable, interoperating with RxJS-style observables, or maintaining existing store-based code. Stores aren’t legacy — they’re the specialized tool.