Shared Reactive Logic
So far, runes have lived inside .svelte components. Svelte 5 lets you use them in plain JavaScript modules too — files named .svelte.js (or .svelte.ts) — which is how you share reactive logic and state across components. This single mechanism covers what was two separate things in the other frameworks: reusable logic (React's custom hooks, Vue's composables) and global state (Svelte 4's stores, Vue's Pinia).
The .svelte.js Module
A regular .js file can't use runes — they're only enabled in files with the .svelte.js extension. That naming tells the compiler "process runes in here too." Once you've done that, $state, $derived, and $effect all work exactly as they do in a component.
A Reusable Factory — Like a Composable
A factory function returning reactive state plus its behavior is Svelte's equivalent of a React custom hook or a Vue composable — call it in a component and each call gets its own independent state. The notable detail: to expose the reactive count while keeping it readable from outside, the returned object uses a getter (get count()). That's because a destructured $state value would lose its reactive connection — the getter re-reads the live value each access. (Vue's composables solve the same problem by returning the ref itself.)
counter.count), or expose a get accessor as above. This is the Svelte equivalent of Vue's "return the ref, not ref.value" rule and React's "return state, not a snapshot." Same underlying concern, slightly different shape.
Shared Global State — A Single Instance
For genuinely global state, export a $state object at module level — there's only one instance, so every component importing it shares the same reactive data. This is Svelte's whole answer to global state management: no Pinia, no Redux, no Context provider — just an exported reactive object. Because objects are deeply reactive (Chapter 2), mutating cart.items.push(...) updates every component using it. This is dramatically lighter than the equivalent in any of the other three.
export const cart = $state({...})) and access its properties, rather than exporting a bare primitive. A directly-exported reactive primitive can't keep its reactive binding across the module boundary — the same getter/object-access concern from earlier. Wrapping shared state in an object (or using a getter) is the reliable pattern.
Svelte Stores Still Exist (and the contrast)
Svelte 4's stores (writable/readable, accessed in components with a $ prefix like {$count}) still work in Svelte 5 and remain useful — especially for RxJS-style reactive streams. But for most shared state, the rune-based .svelte.js approach above is now the recommended default: it's the same mental model as in-component reactivity, with no separate store API to learn. Recognize writable and the $store syntax in existing code; reach for $state modules in new code.
| Need | Svelte 5 | Vue | React |
|---|---|---|---|
| Reusable stateful logic | factory in .svelte.js | composable | custom hook |
| Global shared state | exported $state object | Pinia store | Zustand / Context |
| Each call = own state | factory returns new state | composable | hook |
| Reactive streams (legacy) | svelte/store ($store) | — | — |
Coding Challenges
Write a createCounter factory in a counter.svelte.js module returning { count (getter), increment, decrement, reset }, and use it in two separate components — confirming each gets its own independent count.
📄 View solutionBuild a shared cart in a cart.svelte.js module: an exported $state object with items, plus an addToCart function with the duplicate-quantity check. Use it from a product list and a separate cart-display component, confirming both reflect the same shared state.
📄 View solutionWrite a createLocalStore(key, initial) factory in a .svelte.js module that returns a getter/setter-style reactive value initialized from localStorage and persisted via an $effect on change, then use it to persist a counter so it survives a refresh.
📄 View solutionChapter 10 Quick Reference
- .svelte.js (or
.svelte.ts) — modules where runes work outside components - Factory function returning reactive state + behavior = a composable / custom hook; each call is independent
- Expose reactive values via a getter (
get count()) or by returning the owning object — never a bare value - Exported
$stateobject = global shared state — one instance, no Pinia/Redux/Context needed - Deep reactivity means mutating the shared object (
.push()) updates every consumer - Svelte 4 stores (
writable,$store) still work; rune modules are the modern default - Next chapter: SvelteKit routing (the meta-framework layer)